Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.deep.space/llms.txt

Use this file to discover all available pages before exploring further.

A DeepSpace app is a normal Cloudflare Worker. It serves your React SPA, exposes API routes, and owns a set of Durable Objects that hold per-app data. Around your worker, the DeepSpace platform runs multiple shared services - authentication, payments, integrations, and cross-app data - that your worker talks to over service bindings or HTTPS.
New to DeepSpace? You don’t need to understand the platform internals to build an app. Skip to the Quickstart and return here when you need to add cross-app data sharing, declare custom bindings, or debug a deploy.

The pieces

A DeepSpace deployment has two halves: the worker you write, and the platform workers DeepSpace runs. Your app worker is a Cloudflare Worker compiled from worker.ts and the Vite build of src/. It serves the SPA, handles HTTP and WebSocket routes, and owns the per-app Durable Objects. You deploy it with npx deepspace deploy; it lives at <name>.app.space. The platform workers are managed by DeepSpace; you never deploy or configure them. Your worker talks to three of them at runtime (auth, API, platform) via the helpers in Talking to platform workers; the deploy and dispatch workers sit outside your request path.
Platform workerResponsibility
Auth workerBetter Auth integration, OAuth flows, JWT issuance (ES256, 5-minute lifetime).
API workerStripe billing, the integration proxy (215+ third-party endpoints), user profiles, usage tracking.
Platform workerShared Durable Objects for workspace:*, dir:*, conv:* scopes; R2 file gateway.
Deploy workerReceives deepspace deploy uploads; provisions custom bindings; manages subdomains.
Dispatch workerRoutes *.app.space traffic to the correct deployed app via Workers for Platforms.

Your worker

The scaffolded worker is a Hono app. Its main routes:
RouteWhat it serves
GET /ws/:roomId (and variants)WebSocket upgrades for Durable Object rooms - records, Yjs, canvas, presence, cron
/api/auth/*Proxied to the platform’s auth worker (sign-in, OAuth callback, sign-out)
/api/integrations/*Proxied to the platform’s API worker - any method, including DELETE /oauth/:provider/disconnect
POST /api/actions/:nameServer actions defined in src/actions/index.ts
/api/ai/*Streamed chat (POST /chat) and chat CRUD (POST/PATCH/DELETE /chats[/:id]), defined in src/ai/chat-routes.ts
/api/files/*Scoped R2 file storage, proxied to the platform worker (?scope=app or per-user)
/_deepspace/*Allowlisted same-origin proxy for SDK billing hooks (subscriptions, charges)
/api/debug/*RecordRoom debug endpoints, gated on ALLOW_DEBUG_ROUTES (set by deepspace dev/test, never in production)
Everything elseStatic SPA assets (the Vite build output)
The worker runs on Cloudflare Workers for Platforms, which means each deployed app is isolated in its own namespace. Your app’s URL is <wrangler.toml name>.app.space.

Durable Objects

State that needs to be shared - across users, across tabs, in real time - lives in a Durable Object. The scaffold ships five DO classes; each is an SDK base class subclassed in worker.ts:
ClassPurposeWebSocket route
RecordRoomSQLite-backed records (your collections)/ws/:roomId
YjsRoomPer-document Yjs CRDT state/ws/yjs/:docId
CanvasRoomCollaborative canvas shapes + viewports/ws/canvas/:docId
PresenceRoomCursors, typing indicators, viewports/ws/presence/:scopeId
CronRoomScheduled task scheduler + history/ws/cron/:roomId
You can add more (such as GameRoom for turn-based games) or subclass any of them with custom behavior. A DO instance is identified by its name. Same name = same instance with the same state; a different name is a different instance with its own.

Scopes

A scope is a namespaced identifier that determines which DO instance you’re talking to.
ScopeWhat it representsHosted on
app:<APP_NAME>Your app’s main RecordRoom - everything tied to the appYour worker
conv:<id>A DM or group conversation DOYour worker
workspace:defaultCross-app business data (teams, tasks, people)Platform worker
dir:<appHandle>Cross-app directory (conversations, communities, posts)Platform worker
The default in the scaffold is app:<APP_NAME>, exported as SCOPE_ID from src/constants.ts. Your RecordScope provider mounts this scope; useQuery / useMutations operate against it. To read or write a cross-app scope, see Cross-app shared scopes below.

Talking to platform workers

The SDK exposes three fetch helpers in deepspace/worker for addressing platform services. apiWorkerFetch and platformWorkerFetch prefer a Cloudflare service binding when one is configured and fall back to an HTTPS URL otherwise, so the same code path works in production and under deepspace dev. authWorkerFetch is URL-only by design.
import { authWorkerFetch, apiWorkerFetch, platformWorkerFetch } from 'deepspace/worker'
HelperSignatureUse it for
authWorkerFetch(env, path, init?) => Promise<Response>Sign-in flows, JWT issuance, session cookies. URL-only by design - there is no auth-worker service binding.
apiWorkerFetch(env, path, init?) => Promise<Response>Integration proxy, billing, subscriptions, charges.
platformWorkerFetch(env, pathOrRequest, init?) => Promise<Response>Cross-app shared scopes (workspace:*, dir:*, conv:*), scoped R2 files. Accepts a Request so you can forward c.req.raw verbatim.
Calling c.env.PLATFORM_WORKER.fetch(...) directly works in production but breaks under deepspace dev, where the binding is absent and the CLI writes a PLATFORM_WORKER_URL fallback into .dev.vars for the helpers to pick up.

Security model - WebSocket identity

Durable Objects trust whatever identity the request URL carries. Verifying that identity is the worker’s job - it strips anything the client sent, then re-applies it from a verified JWT. The SDK does this at two entry points: Per-app WebSocket route (wsRoute) - the scaffold ships an inline wsRoute helper in worker.ts (it is not an SDK export). It strips userId, userName, userEmail, userImageUrl, role, and token from the URL on every upgrade, then re-applies identity only from a verified JWT. Three states are possible:
  • No token → anonymous (DO assigns anon-<uuid>)
  • Invalid token → 401
  • Valid token → identity = JWT sub / name / email / image
Platform worker (cross-app scopes) - when a workspace:*, dir:*, or conv:* request hits the platform worker, the same URL parameters are stripped and re-applied from the JWT before it forwards to the shared DO. On HTTP forwards, x-user-id is overwritten from the JWT and x-app-action is dropped. These scopes require a valid JWT; unauthenticated requests are rejected with 401.
Never put identity in WebSocket URLs or /api/* headers. The scaffold’s inline wsRoute helper strips them; identity always comes from the JWT subject. There is no client-side override.
/ws/yjs/:docId is special. It is the only /ws/* route that requires a verified JWT (401 without one) and resolves a per-doc role (admin / member / viewer) from the documents collection’s ownerId, editors, and collaborators fields - 403 when the caller has none. Do not collapse it into a bare wsRoute call. See YjsRoom authentication and roles.

Cross-app shared scopes

If your app needs to read or write workspace:*, dir:*, or conv:* scopes that sync across DeepSpace apps, three edits are required.
1

Declare the platform service binding

Add this to wrangler.toml:
[[services]]
binding = "PLATFORM_WORKER"
service = "deepspace-platform"
2

Proxy shared scopes in worker.ts

Wrap the scaffold’s inline wsRoute helper in a small router so cross-app scopes go to the platform instead of your DO:
import { platformWorkerFetch } from 'deepspace/worker'

app.get('/ws/:roomId', async (c) => {
  const roomId = c.req.param('roomId')
  if (/^(workspace|dir|conv):/.test(roomId)) {
    return platformWorkerFetch(c.env, c.req.raw)
  }
  return wsRoute((env) => env.RECORD_ROOMS)(c)
})
3

Mount the shared scope in your provider tree

import { RecordScope } from 'deepspace'
import { WORKSPACE_SCHEMAS } from 'deepspace/worker'
import { SCOPE_ID, APP_NAME } from './constants'
import { schemas } from './schemas'

<RecordScope
  roomId={SCOPE_ID}
  schemas={schemas}
  appId={APP_NAME}
  sharedScopes={[{ roomId: 'workspace:default', schemas: WORKSPACE_SCHEMAS }]}
>
  <App />
</RecordScope>
WORKSPACE_SCHEMAS is exported from deepspace/worker alongside the other schema constants. Importing it in client code is safe - Vite tree-shakes the worker-only modules.
Without all three edits, sharedScopes writes to your app’s own DO instead of the platform’s shared DO, and cross-app data won’t show up.

Build & deploy pipeline

npx deepspace deploy performs these steps in order:
1

Build with Vite

npx vite build runs the Cloudflare Workers Vite plugin, producing the client assets and the worker bundle in a single pass, plus a normalized wrangler.json under .wrangler/deploy/.
2

Extract manifests

The CLI reads the DO bindings, custom bindings (R2, KV, D1, Vectorize, AI, …), and user secrets from .dev.vars (below the SDK-managed divider) out of the build output.
3

Validate the binding manifest

validateBindingManifest checks custom bindings against allowed types and reserved names. Reserved or duplicate names abort the deploy with a file-pointing error.
4

Upload to the deploy worker

Worker bundle, assets, DO manifest, custom bindings, and user secrets are POSTed as a single FormData to the deploy worker.
5

Auto-provision resources

Server-side, the deploy worker creates any binding declared with id = "auto" (or bucket_name = "auto", etc.) on the platform Cloudflare account on first deploy.
6

Register subdomain and dispatch route

The worker is loaded into the dispatch namespace under <name>.app.space; user secrets become secret_text bindings on the deployed worker. The dispatch worker routes incoming traffic to your worker’s isolate.
7

Sync subscription plans and products (optional)

If src/subscriptions.ts or src/products.ts exist, the CLI bundles them with esbuild and posts the declarations to the API worker. Skipped silently when the files are absent.

Next steps