@suluk/provision
v0.3.0
Published
Declarative service provisioning for a Suluk app, modelled on the Open Service Broker API and driven like drizzle-kit. Declare the infra you want in one provision.config (D1/KV/R2/secrets/Stripe/domains/tokens as OSB brokers); `plan` diffs desired-vs-live
Readme
CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.
Install
bun add @suluk/provisionWhat it does
Infrastructure as a declarative config, modelled on the Open Service Broker API
and driven like drizzle-kit (C047). You list the instances you want — each an OSB broker plus a
name, plan, params, and binding map — and the lifecycle is a pure function of that config × live state:
plandiffs desired-vs-live into a reviewable list of steps (create/update/noop/deprovision), in binding-DAG order. Pure: no provider calls, no clock.applywalks that DAG and calls the OSB verbs —provision(idempotent) → poll asynclastOperationuntil a create settles →bindfor credentials → land them in aBindingSink(the@suluk/envmanifest in prod). A downstream@ref.keyparam resolves to a freshly provisioned value, so the DAG wires itself.checkDrift/assertNoDriftis the CI gate (a clean plan ⇒ no drift).
A param of the form @<ref>.<key> is a binding reference — that's what makes db provision
before the token that scopes to @db.database_id. defineProvision validates the static shape
(unique refs, an acyclic binding DAG) at authoring time, so a loop or an unreferenceable ref is
caught before any provider is called. Stateful resources can be protected (the terraform
prevent_destroy analog). A drizzle-style snapshot + migration model gives repeatable,
documentable steps.
Usage
import {
defineProvision, plan, apply, assertNoDrift,
memoryStore, memorySink,
cloudflareD1, cloudflareToken,
} from "@suluk/provision";
// Declare the infra. `@db.database_id` wires the token AFTER the D1 database.
const config = defineProvision({
instances: [
{ ref: "db", service: "cloudflare-d1", name: "app-db", protected: true, bind: { database_id: "CLOUDFLARE_D1_ID" } },
{ ref: "token", service: "cloudflare-token", name: "d1-token",
params: { scope: "@db.database_id" }, bind: { token: "CLOUDFLARE_D1_TOKEN" } },
],
});
// The concrete Cloudflare brokers wrap @suluk/cloudflare's idempotent provisioners.
const brokers = { "cloudflare-d1": cloudflareD1(cf), "cloudflare-token": cloudflareToken(cf) };
const store = memoryStore(); // a JSON file in prod (`fileStore`)
const sink = memorySink(); // the @suluk/env manifest in prod (`envSink`)
// Review, then execute along the binding DAG.
const state = await store.load();
console.table(plan(config, state).steps); // [{ ref, action, reason }, …]
const result = await apply(config, { brokers, store, sink });
result.outputsByRef.db; // { database_id: "…" } — resolved outputs after the run
// CI gate: throw if the live infra has drifted from the config.
await assertNoDrift(config, await store.load());CLI
suluk-provision plan # diff desired-vs-live
suluk-provision apply # provision → bind → land credentials
suluk-provision check # drift / orphan gate (CI)
suluk-provision statusWhat's inside
| Export | What it does |
| --- | --- |
| defineProvision(config) | Validate + return the config (throws on dup/unknown ref or a cycle). |
| plan(config, state, prune?) | The pure diff → ProvisionPlan (steps, orphans, clean). |
| apply(config, opts) | Execute a plan along the binding DAG → ApplyResult (with resolved outputs). |
| checkDrift / assertNoDrift | Drift report + the CI gate. |
| pull / reconcile / discover | Read live state back; adopt un-tracked instances. |
| teardown | Deprovision (skips protected unless forced). |
| topoOrder, parseRef, depsOf, resolveParams, fingerprint | The binding-DAG + ref-resolution internals. |
| snapshot / diffSnapshots / migrate / generate | The drizzle-style snapshot + migration model. |
| memoryStore / fileStore, memorySink / envSink | The state store + binding sink (dev/test vs prod). |
| cloudflareD1 / cloudflareKv / cloudflareR2 / cloudflareSecrets / cloudflareToken / cloudflarePagesDomain | The concrete Cloudflare brokers. |
| defineProvisionApp, runCli | The drizzle-kit-style app config + CLI. |
Types Broker, BindingSink, StateStore, Catalog, InstanceSpec, InstanceState, and the OSB
request/result shapes are exported alongside.
Boundary
@suluk/provision is the OSB client / orchestrator — a layer above
@suluk/cloudflare (the idempotent provisioners it wraps as brokers),
@suluk/deploy, and @suluk/env (the binding sink). It orchestrates;
they execute. Provider calls live in the brokers; the core is pure orchestration over them, with the
clock and sleep injected so apply is deterministically testable. It's the lower half of
@suluk/platform, which merges each module's provision fragment into one config.
License
Apache-2.0
