Skip to content

Writing a custom provisioner

ss provision is extensible. Any cloud API, local tool, or third-party service can be added as a provisioner by implementing the ProvisionerAdapter interface and registering it with the registry.

import type { ResolvedService } from "@celestial/starsystem/server";
import type {
ProvisionerAdapter,
ProvisionContext,
ProvisionResult,
ProvisionStatus,
ProvisionStateRecord,
} from "@celestial/starsystem/provisioners";
export class MyProvisioner implements ProvisionerAdapter {
/** Unique provider name. Must match `provision.provider:` in YAML or `target.type:` */
readonly provider = "my-provider";
/**
* Set to false if your provisioner doesn't need an API key from the vault.
* Defaults to true (vault lookup happens before provision() is called).
*/
readonly requiresApiKey = true;
/** Return true if this provisioner can handle the given service */
canProvision(service: ResolvedService): boolean {
return service.target?.type === "my-provider";
}
/**
* Create or refresh the resource.
* ctx.existing is set when the resource was previously provisioned.
* Idempotent: if ctx.existing is set, refresh credentials without re-creating.
*/
async provision(ctx: ProvisionContext, service: ResolvedService): Promise<ProvisionResult> {
const isNew = !ctx.existing;
// Your API call here
const resource = isNew
? await myApi.create({ name: ctx.serviceId, apiKey: ctx.apiKey })
: await myApi.get(ctx.existing!.resourceId, ctx.apiKey);
// Optionally save partial state mid-run to survive failures
await ctx.savePartialState?.({ resourceId: resource.id });
return {
state: {
provider: "my-provider",
resourceId: resource.id,
provisionedAt: new Date().toISOString(),
region: resource.region,
metadata: { name: resource.name },
},
credentials: {
// These are written to the vault only — never to YAML
MY_SERVICE_URL: resource.url,
MY_SERVICE_API_KEY: resource.apiKey,
},
summary: isNew
? `created ${resource.name} (${resource.id})`
: `${resource.name} already exists`,
};
}
/** Check the health of a provisioned resource */
async status(ctx: ProvisionContext, state: ProvisionStateRecord): Promise<ProvisionStatus> {
try {
const resource = await myApi.get(state.resourceId, ctx.apiKey);
return {
healthy: resource.status === "active",
message: `status: ${resource.status}`,
raw: resource,
};
} catch (err) {
return { healthy: false, message: String(err) };
}
}
/** Tear down the resource. Irreversible. */
async destroy(ctx: ProvisionContext, state: ProvisionStateRecord): Promise<void> {
await myApi.delete(state.resourceId, ctx.apiKey);
}
}
FieldTypeDescription
projectstringStarsystem project name (from name: in starsystem.yaml)
envstringActive environment (e.g. "local", "prod")
serviceIdstringService ID within the system
apiKeystringAPI token read from vault via providers:<name> api_key
existingProvisionStateRecord | undefinedState from previous run (undefined on first run)
configDirstring | undefinedDirectory containing starsystem.yaml (for resolving relative paths)
vaultGet(key: string) => Promise<string | null>Read any vault key
vaultSetProviderKey(provider, key, value) => Promise<void>Write a provider key back to vault
savePartialState(partial) => Promise<void>Persist partial state mid-run

Register it once at startup in the provisioner registry:

// In your app's entry point or plugin setup
import { provisioners } from "@celestial/starsystem/provisioners";
import { MyProvisioner } from "./my-provisioner.js";
provisioners.register(new MyProvisioner());

Or extend the registry in packages/starsystem-builder/src/provisioners/registry.ts for built-in provisioners:

import { MyProvisioner } from "./my-provisioner.js";
constructor() {
// ... existing registrations
this.register(new MyProvisioner());
}

Once registered, use it in starsystem.yaml via provision.provider::

services:
my-service:
type: external
name: My Service
provision:
provider: my-provider
plan: starter
region: us-east-1

Or as a deployment target via target.type: in a v2 overlay:

starsystem.prod.yaml
services:
my-service:
target:
type: my-provider
credential_env: MY_SERVICE_URL
FieldWhere it goesNotes
stateVault + overlay YAML _state blockSafe to commit — no secrets
credentialsVault onlyNever written to YAML
summaryCLI output + MCP responseHuman-readable one-liner

The state block is written back to the overlay YAML as a _state: entry, giving you a human-readable record of what was provisioned:

# starsystem.prod.yaml (written by ss provision)
services:
my-service:
provision:
_state:
provider: my-provider
resourceId: res_abc123
provisionedAt: "2026-04-27T12:00:00Z"
region: us-east-1
metadata:
name: my-service-prod
ProvisionerFileHandles
NeonProvisionerprovisioners/neon.tsNeon serverless Postgres
SupabaseProvisionerprovisioners/supabase.tsSupabase cloud projects
SupabaseLocalProvisionerprovisioners/supabase-local.tsLocal Supabase CLI stack
FlyProvisionerprovisioners/fly.tsFly Machines deployments
RailwayProvisionerprovisioners/railway.tsRailway services + databases
LocalProcessProvisionerprovisioners/local-process.tsLocal shell processes
CloudflareProvisionerprovisioners/cloudflare.tsCF custom hostnames, DNS, Pages

Use NeonProvisioner as the reference for a simple API-based provisioner, and FlyProvisioner for a multi-step provisioner that uses savePartialState.