Skip to main content
Documentation

Deployment

How DeepSpace deploys your app to Cloudflare Workers for Platforms.

On this page

npx deepspace deploy builds your app and uploads it to Cloudflare Workers for Platforms, returning a live <name>.app.space URL.

What deploy does#

The CLI builds your app with Vite (the Cloudflare plugin produces both the client bundle and the worker), validates your binding manifest, and uploads everything to the deploy worker as one request. From there, the platform auto-provisions any binding marked "auto", binds every value in the selected secrets config as a secret_text binding, and registers the worker in the dispatch namespace at <name>.app.space.

Deploys are idempotent in the sense that matters: re-running deploy with no source changes reuses provisioned resources and re-uploads nothing — unchanged assets are already in the content-addressed store. It is not a no-op, though. Every deploy appends a release fact, so a byte-identical redeploy still advances the release history and is still something you can roll back to. See Releases and rollback.

See Build & deploy pipeline for the full step-by-step. Inspect deployed apps, logs, and traffic in the web dashboard at dashboard.deep.space.

App identity#

DEEPSPACE_APP_ID in wrangler.toml is the app's immutable identity — data, secrets, collaborators, billing, and custom domains all key to it. The name field is only a lease on <name>.app.space.

A repo without an id needs no setup step: the first deploy mints one and writes it into wrangler.toml. Commit that change. The id is the app's permanent identity, not a secret, and a checkout without it is a different app waiting to be minted. To stamp the id without deploying, run npx deepspace app init; to fork a cloned repo into a separate app with its own data and secrets, run npx deepspace app init --new-id.

Subdomains#

<name>.app.space is a fully production-grade URL with SSL - most apps ship and stay on it. The name field in wrangler.toml is your subdomain:

name = "my-app"
# → deploys to https://my-app.app.space
toml

Rules:

  • Lowercase alphanumeric with optional dashes, 2-63 characters; cannot begin with a dash
  • Non-conforming names cause the CLI to fail with a clear error and the suggested canonical form; fix name in wrangler.toml and re-run
  • Names are globally unique. One already claimed by another app is refused with The name <host> is taken by another app.
  • Changing name and re-deploying renames the app rather than creating a second one. The URL moves; data, secrets, and collaborators travel with it, because they key to the immutable DEEPSPACE_APP_ID rather than to the name. The CLI prompts to confirm, or pass --rename to confirm non-interactively.
  • After a rename, the old subdomain stays reserved for its previous owner for 30 days. You (or the same app) can reclaim it at any point in that window; only after it expires can another account take the name.

If you'd rather use your own domain (e.g. myapp.com), see Custom domains.

State preservation#

A deploy does not wipe Durable Object state. Your records, conversations, files, and cron history persist across deploys.

Survives a deploy:

  • Durable Object SQLite tables (records, Yjs documents, canvas state, cron history)
  • R2 bucket contents
  • Auto-provisioned bindings (D1, KV, Vectorize, etc.). Resource IDs are persisted and reused.
  • The app's secrets store. Every deploy re-binds its values as secret_text bindings.

Does not survive a deploy:

  • Worker module-level globals
  • In-memory caches inside DOs. Every DO restarts; persistent storage stays, in-memory state is rebuilt on the next request.

Schema migrations#

The SDK runs additive schema migrations automatically. When a DO cold-starts after a deploy, the per-collection migrator creates any new tables and runs ALTER TABLE ADD COLUMN for new fields. Re-running with an unchanged schema is a no-op.

Destructive changes are not handled: dropping a column, changing a column type, renaming a field, or backfilling data into a new shape. You're responsible for any data migration before deploying a breaking schema change.

Secrets come from the store, not from .dev.vars#

Every app has one platform-owned, encrypted secrets store, keyed by DEEPSPACE_APP_ID. That store is the only secrets input to a deploy: deploy pulls the selected config, binds each value as a Cloudflare secret_text binding, and reconciles the worker's bindings against the store — a key deleted from the store disappears from the live worker on the next deploy. Worker code reads env.<NAME> identically in dev and production, and values never appear in compiled assets or client bundles.

.dev.vars is a generated plaintext cache of that store, rewritten whole by the CLI — hand edits vanish, deploy never reads it back, the scaffold's .gitignore keeps it uncommitted, and store changes reach a deployed app on the next deploy and a running dev session on restart (cache behavior). A missing secrets config (prd, or <name> for [env.<name>]) refuses deploy with secrets_config_missing and an executable fix, while an explicitly created empty one deploys and removes every user-secret binding — that distinction, plus name rules and caps, lives in the secrets guide's Missing is not empty and Names and caps.

APP_IDENTITY_TOKEN#

One generated key deserves a note. APP_IDENTITY_TOKEN authenticates app-origin platform calls — payments, files, and screenshot APIs — and appears in .dev.vars only once the app is registered with the platform. Deploy registers it, and an earlier secrets write registers it too (the first write claims the app id), so it can exist before the first deploy. Until one of those happens, those local APIs run without app-origin authentication — if a payments or files call fails on a brand-new app, register the app first.

Named environments#

A [env.<name>] block in wrangler.toml is not a variant of your app — it is a separate app: its own canonical name, its own DEEPSPACE_APP_ID, its own Durable Objects, and its own secrets config. Initialize it explicitly, or let its first deploy mint the id:

npx deepspace app init --env staging     # stamp [env.staging] with its own id
npx deepspace deploy --env staging       # deploys <staging-name>.app.space with secrets config "staging"
bash

Commit the minted id, same as the top-level one. Remove the environment with npx deepspace app undeploy --env staging when it has served its purpose.

Wrangler environments do not inherit#

Wrangler named environments do not inherit vars, Durable Object bindings and migrations, assets, or KV/R2/D1 declarations from the top level. Repeat every required block under [env.<name>], or the deployed worker boots without them:

name = "my-app"

[vars]
DEEPSPACE_APP_ID = "app_..."

[env.staging]
name = "my-app-staging"

[env.staging.vars]
DEEPSPACE_APP_ID = "app_..."    # the staging app's own id

# Repeat the durable_objects bindings, migrations, assets, and any
# KV/R2/D1 blocks the top level declares, under [env.staging.*].
toml

The browser bundle needs the environment's app id#

Server code gets the right id per environment through env.DEEPSPACE_APP_ID. The browser bundle does not — if src/constants.ts hardcodes the production id, a staging build connects the browser to production rooms while staging server actions write to staging rooms. The current scaffold injects the active id at build time, from the wrangler config the build targets:

// src/constants.ts
declare const __DEEPSPACE_APP_ID__: string
export const APP_ID: string = __DEEPSPACE_APP_ID__
export const SCOPE_ID = `app:${APP_ID}`
ts
// vite.config.ts
import { deepspaceBuild } from 'deepspace/build'
// …
plugins: [cloudflare(), deepspaceBuild({ appDir }), /* … */]
ts
// vitest.config.ts
import { appIdDefine } from 'deepspace/build'
export default defineConfig({ define: appIdDefine({ appDir }), /* … */ })
ts

Three files, one change - src/constants.ts on its own builds clean and then throws ReferenceError in the browser. deploy refuses a bundle that carries another app's id (app_id_env_mismatch) or the unreplaced define (app_id_define_unsubstituted), and the refusal prints this retrofit; app update reports the same retrofit for an older app as the 2026-08-build-injected-app-id migration guide - see Updating an app.

Gate staging-only routes on an explicit staging signal (an env-derived flag), never on "not production" heuristics.

When is a deploy actually live?#

deploy does not return until it has checked, and it tells you what it established rather than assuming:

servingMeans
confirmedTen consecutive fresh connections all answered with this release
unconfirmedSome requests still get the previous release — it is rolling out
unverifiableThis release carries no stamp (older platform, or a resumed deploy), so the CLI cannot tell

The mechanism: every release ships a stamp at /.well-known/deepspace/release.json carrying its own identity, served no-store. After uploading, the CLI polls that path and requires several agreeing answers over separate connections.

confirmed means confirmed from where the CLI is standing. Other regions may still be rolling over — per-colo propagation is not something a client can force, and this is best-effort rather than a guarantee. If you are asserting against a fresh deploy in CI, treat unconfirmed as "wait and retry", not as a failure.

This is also why a browser tab open across a deploy can ask for a hashed asset that no longer exists. Missing assets answer 404 rather than the app shell, and the scaffold reloads once on Vite's vite:preloadError so a lazily-loaded route recovers instead of silently dying.

Custom bindings#

Add Vectorize, Workers AI, R2, KV, D1, Queues, Browser Rendering, Hyperdrive, or Analytics Engine bindings by declaring them in wrangler.toml. Set the ID to "auto" to auto-provision:

[[vectorize]]
binding = "VEC"
index_name = "auto"
dimensions = 768
metric = "cosine"

[[d1_databases]]
binding = "MY_DB"
database_id = "auto"
database_name = "my-app-db"
toml

The deploy worker provisions the resource the first time you deploy, persists the ID, and reuses it on every subsequent deploy. See Custom bindings for the full list of types and gotchas.

Undeploy#

To take an app down:

npx deepspace app undeploy
bash

This removes the subdomain registration, tears down auto-provisioned resources (with one exception), and cleans up bindings. The exception:

  • R2 buckets with files in them are not auto-deleted. This protects against accidental user-content loss. Empty the bucket manually if you want it gone.

Durable Object data is removed when the worker is undeployed. Back up anything you want to keep.

Undeploy is not deletion of the app itself: the app id, its registration, collaborators, and its secrets store remain, so a later deploy of the same id revives the same app.

Before your first deploy#

Three things to verify before shipping (the product-polish checklist covers the UI side):

  • Settle the app name. Renaming later is safe — data, secrets, and collaborators follow the app id (subdomain rules) — but every rename moves your public URL.
  • Audit the secrets store. npx deepspace secrets list shows exactly what ships as secret_text bindings. Confirm production credentials, not test keys.
  • Run npx deepspace test run. Playwright runs your specs against a local build before you deploy them against the live one.

Next steps#