Deploy, plan, promote
Shipped
Once an extension passes its tests, you ship it.
Turbobase supports two deploy styles: a quick single-file deploy for one
extension, and config-as-code for the whole instance — where your
repository is the desired state, plan diffs it against an instance, and
deploy --to applies it. turbobase logs closes the loop after a deploy.
Single-file deploy
Section titled “Single-file deploy”The fast path — push one authored extension straight to the instance:
turbobase ext deploy hooks/customer.before_save.ts# deployed ns:customer:require-name → status=active (created).The CLI parses the file’s metadata (id / recordType / event / source / kind),
POSTs it to /v1/extensions, and prints the resulting id, status, and
outcome. A non-admin personal access token (PAT) gets a deploy forbidden (403).
Config-as-code — your repository is the desired state
Section titled “Config-as-code — your repository is the desired state”For everything else, the unit of deploy is your project tree. These four
inputs are the config-as-code source of truth for an instance’s deployable
surfaces, and plan, deploy --to and promote all build from them:
| In your repo | Becomes |
|---|---|
hooks/**/*.ts (defineHook) |
hooks — including nested folders, which is what ext new hook writes |
conduits/**/*.ts (defineConduit) |
conduits |
turbobase/sync.json |
promoted-field ownership + mirrored tables |
turbobase/schedules.json |
scheduled jobs |
turbobase apps init scaffolds all four, so a project has them from the first
commit. Nothing has to be generated first — a plan reads the branch you are on,
which is what lets a pull request show what merging would change.
Adopting an instance that already has config
Section titled “Adopting an instance that already has config”If your instance was configured before the repo existed — hooks deployed one file at a time, a schedule set up in the console — pull it down first:
turbobase ext export# wrote turbobase/bundle.json (142 lines).ext export calls GET /v1/config/export (needs extensions:deploy; a PAT
without it gets a clear 403). This is the adopt path, not the authoring one:
move the extensions it lists into hooks/ and conduits/, and the file stops
being consulted. A project that authors nothing in-tree still falls back to
turbobase/bundle.json, so nothing breaks while you migrate.
plan — diff before you write
Section titled “plan — diff before you write”plan prints a terraform-style diff of this project against a target env’s
live config. It writes nothing — run it on every pull request:
turbobase ext plan --to prod# plan against prod:# + hook ns:customer:require-name# ~ conduit shop2ns:order# - hook custom:legacy:foo## 1 to add, 1 to change, 1 to destroy.--from <env> is advisory (it just labels which env the config was authored
against); your project is always the desired state and --to selects the env
whose live state you diff against. An empty plan prints
(no changes — up to date.).
A - line means the instance has something your repo does not declare. Reading
those before merging is the point of planning on a pull request — but note
that deploy will not act on them unless you ask (see below).
deploy --to — apply the whole bundle
Section titled “deploy --to — apply the whole bundle”To apply the bundle (idempotently) to an env:
turbobase ext deploy --to sandbox# applied bundle to sandbox: +1 ~1 -1 (2 backfill(s) queued)deploy --to builds the manifest from your project tree and POSTs it to
/v1/config/apply, reporting adds / changes and any backfills queued. <file>
is ignored when --to is given. As with single-file deploy, a non-admin PAT gets
a 403.
Removal is opt-in
Section titled “Removal is opt-in”deploy --to adds and updates. It does not remove. Deleting a hook from your
repo does not delete it from the instance; the run prints what it declined to
remove:
turbobase ext deploy --to prod# applied bundle to prod: +1 ~1 -0# 2 resource(s) exist on prod but are not declared by this project — NOT removed:# hook custom:legacy:foo# schedule nightly-legacy-export# Add them to this repo (`turbobase ext export` pulls them down), or re-run with --prune to remove them.Pass --prune to take them:
turbobase ext deploy --to prod --prune# applied bundle to prod: +1 ~1 -2The safe default exists because a repo does not always declare everything already
running on an instance. Without it, the first deploy --to from a newly
scaffolded project — which declares one example hook — would delete every other
hook, conduit, promoted field, mirrored table and scheduled job on that instance.
Once your repo IS the whole truth for your config, add --prune to your CI step.
promote takes --prune too, with the same default.
A prune never leaves a schedule running a process you removed
Section titled “A prune never leaves a schedule running a process you removed”One removal is refused rather than taken: dropping a process while a recurring job on the instance still runs it. The job cannot do anything useful once the process is gone — it fires on its schedule, fails every time, and switches itself off after five failures — so the deploy stops instead:
turbobase ext deploy --to prod --prune# turbobase: bundle apply failed: POST /v1/config/apply → 400: invalid_bundle:# schedule 'rollup-tick' runs the process 'custom:process:nightly-rollup', which# this deploy would remove — the schedule would keep firing, fail every time and# switch itself off after five failures. Keep the process in the bundle, point# the schedule at another process in the same deploy, or drop the schedule in# the same deploy (delete it on the instance if it is not the bundle's)That whole sentence reaches your terminal, ways out included — the refusal is the instance’s own words and the CLI no longer shortens one.
Nothing is written when it refuses — not the additions earlier in the same plan. Three ways out, and the right one depends on what you meant:
- Retire both. Delete the schedule from
turbobase/schedules.jsonin the same commit as the process. One deploy, both gone — this is the supported way. - Move the job. Point the schedule at a different process in the same commit. The job is screened against the body it will have once the deploy lands, so repointing it at a process you keep — or one this same bundle brings — applies normally, and so does turning it into a chat-prompt or system job.
- Keep the process. Leave it declared and the prune carries on with everything else.
A job somebody created in the app rather than in your repo is not yours to drop from the bundle; delete it on the Scheduled tasks page and re-run the deploy.
A process no job runs prunes exactly as before, and so does one whose job was already dead before this deploy: the refusal is about an orphan your deploy would create, never one it inherited.
promote — env→env behind a fail-closed gate
Section titled “promote — env→env behind a fail-closed gate”Promotion moves a vetted bundle from one env into another — typically
sandbox → prod. It is guarded by a fail-closed confirm-slug check:
turbobase ext promote --from sandbox --to prod --confirm acme# promoted sandbox → prod (acme): +3 ~2 -0If --confirm does not exactly match the target instance’s slug, the CLI
rejects before any network write — nothing is POSTed:
turbobase ext promote --from sandbox --to prod --confirm wrongslug# turbobase: promote refused (fail-closed): …This is the customer promoting their own instance config across their own environments. It is unrelated to — and never touches — the control-plane’s production cloud deploys.
Close the loop — turbobase logs
Section titled “Close the loop — turbobase logs”After a deploy, watch the extension run:
turbobase logs --subsystem hooks --since 10m # every hook outcometurbobase logs --extension ext_abc123 # this extension's log bufferturbobase logs --conduit shop2ns:order --follow # a CONDUIT's run ledgerWith no target flag, logs reads the unified platform event log — every hook
rejection and error, every guardrail verdict, every conduit run, in one stream
with one correlation id. --extension <id> reads that extension’s bounded log
ring buffer ([level] ts message per line) and --conduit <id> streams the
conduit_event run ledger, polling every --interval-ms (default 2000).
These two flags replace turbobase ext logs, which is removed.
The full workflow
Section titled “The full workflow”turbobase ext export # 1. snapshot live config# edit hooks/ + conduits/, then:turbobase ext lint hooks/foo.ts # 2. lint offlineturbobase ext test hooks/foo.ts # 3. dry-run in the sandboxturbobase ext plan --to sandbox # 4. preview the diffturbobase ext deploy --to sandbox # 5. apply to sandboxturbobase ext promote --from sandbox --to prod --confirm acme # 6. promoteturbobase logs --extension ext_abc123 # 7. verify it runsNext steps
Section titled “Next steps”- Sandboxes and testing — the steps before deploy.
- Marketplace — install / publish instead of authoring locally.
- CLI reference — every flag.