> ## 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.

# Architecture

> How DeepSpace apps are structured, deployed, and connected to the platform.

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.

<Info>
  **New to DeepSpace?** You don't need to understand the platform internals to build an app. Skip to the [Quickstart](/get-started/quickstart) and return here when you need to add cross-app data sharing, declare custom bindings, or debug a deploy.
</Info>

## 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](https://vite.dev/) 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](#talking-to-platform-workers); the deploy and dispatch workers sit outside your request path.

| Platform worker     | Responsibility                                                                                     |
| ------------------- | -------------------------------------------------------------------------------------------------- |
| **Auth worker**     | Better Auth integration, OAuth flows, JWT issuance (ES256, 5-minute lifetime).                     |
| **API worker**      | Stripe billing, the integration proxy (215+ third-party endpoints), user profiles, usage tracking. |
| **Platform worker** | Shared Durable Objects for `workspace:*`, `dir:*`, `conv:*` scopes; R2 file gateway.               |
| **Deploy worker**   | Receives `deepspace deploy` uploads; provisions custom bindings; manages subdomains.               |
| **Dispatch worker** | Routes `*.app.space` traffic to the correct deployed app via Workers for Platforms.                |

## Your worker

The scaffolded worker is a [Hono](https://hono.dev/) app. Its main routes:

| Route                            | What it serves                                                                                                    |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `GET /ws/:roomId` (and variants) | WebSocket upgrades for Durable Object rooms - records, Yjs, canvas, presence, cron, jobs                          |
| `/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/:name`        | Server 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 else                  | Static 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 six DO classes; each is an SDK base class subclassed in `worker.ts`:

| Class          | Purpose                                                     | WebSocket route         |
| -------------- | ----------------------------------------------------------- | ----------------------- |
| `RecordRoom`   | SQLite-backed records (your collections)                    | `/ws/:roomId`           |
| `YjsRoom`      | Per-document Yjs CRDT state                                 | `/ws/yjs/:docId`        |
| `CanvasRoom`   | Collaborative canvas shapes + viewports                     | `/ws/canvas/:docId`     |
| `PresenceRoom` | Cursors, typing indicators, viewports                       | `/ws/presence/:scopeId` |
| `CronRoom`     | Scheduled task scheduler + history                          | `/ws/cron/:roomId`      |
| `JobRoom`      | Durable background jobs (AI generations, exports, fan-outs) | `/ws/jobs/: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.

| Scope               | What it represents                                      | Hosted on       |
| ------------------- | ------------------------------------------------------- | --------------- |
| `app:<APP_NAME>`    | Your app's main RecordRoom - everything tied to the app | Your worker     |
| `conv:<id>`         | A DM or group conversation DO                           | Your worker     |
| `workspace:default` | Cross-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](#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.

```ts theme={null}
import { authWorkerFetch, apiWorkerFetch, platformWorkerFetch } from 'deepspace/worker'
```

| Helper                | Signature                                          | Use 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.

<Warning>
  **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.
</Warning>

<Warning>
  **`/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](/guides/collaborative-editing#yjsroom-authentication-and-roles).
</Warning>

## 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.

<Steps>
  <Step title="Declare the platform service binding">
    Add this to `wrangler.toml`:

    ```toml theme={null}
    [[services]]
    binding = "PLATFORM_WORKER"
    service = "deepspace-platform"
    ```
  </Step>

  <Step title="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:

    ```ts theme={null}
    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)
    })
    ```
  </Step>

  <Step title="Mount the shared scope in your provider tree">
    ```tsx theme={null}
    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.
  </Step>
</Steps>

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:

<Steps>
  <Step title="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/`.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## Next steps

* [Data model](/concepts/data-model) - collections, records, and how data is shaped.
* [Permissions](/concepts/permissions) - role-based access control on collections.
* [Real-time sync](/concepts/realtime-sync) - how WebSocket sync works under the hood.
* [Deployment](/concepts/deployment) - what happens when you run `deploy`.
