Skip to main content
Documentation

Project structure

A tour of the files in a scaffolded DeepSpace app.

On this page

A scaffolded app is a Vite + React project with a Cloudflare Worker entry point. This page lists what npm create deepspace@latest ships and what each file is for.

Top-level files#

FilePurpose
worker.tsHono worker and Durable Object class declarations. Edit to add custom routes or DO classes.
wrangler.tomlCloudflare config. Holds the app's immutable DEEPSPACE_APP_ID under [vars], the name (subdomain label), DO bindings and migrations, and any custom bindings.
index.htmlHTML shell. Set <html data-theme="..."> to pick a theme, update <title> and favicon.
vite.config.tsVite + @cloudflare/vite-plugin + generouted config.
vitest.config.tsVitest config for the unit suite.
eslint.config.jsFlat ESLint config.
postcss.config.jsPostCSS pipeline for Tailwind v4.
tsconfig.jsonTypeScript config. Strict mode, covers src/ and worker.ts.
package.jsonDependencies and the deepspace-backed npm scripts (dev, build, test, deploy).
deepspace.migrations.jsonApp-owned ledger of which platform migration guides this project has applied and validated. app update reads it but never writes it.
skills-lock.jsonPins the revision of the agent skill installed under .agents/.
AGENTS.mdAgent-facing project brief, read by tools that follow the AGENTS.md convention.
CLAUDE.mdClaude Code instructions - points at the bundled DeepSpace skill.
.gitignoreIgnores node_modules, dist, .wrangler, .dev.vars*, .deepspace, and the generated src/router.ts / src/modals.tsx.
.dev.varsLocal secrets (gitignored). Not in the template - deepspace dev start writes SDK-managed keys here on first run.

Agent tooling#

PathPurpose
.agents/skills/deepspace/The bundled DeepSpace agent skill: SKILL.md, topic references (auth, deploy, schemas, payments, ...), and integration manifests. Installed by the scaffolder; version pinned in skills-lock.json.
.claude/launch.jsonClaude Code launch configuration (gitignored).

src/ - application code#

Pages and layouts#

Routing is file-based via generouted: every file under src/pages/ becomes a route. Folders in parentheses are route groups - they apply a layout without appearing in the URL.

FileRoutePurpose
src/main.tsx-Vite entry. Mounts <Routes /> into #root.
src/pages/_app.tsx-Outermost shell (toasts, error boundary).
src/pages/index.tsx/Static landing page. No providers - no auth fetch, no WebSocket.
src/pages/[...all].tsx*Catch-all 404.
src/pages/(app)/_layout.tsx-The dynamic-app boundary. Mounts DeepSpaceAuthProviderAuthBootRecordProviderRecordScope, plus the Navigation chrome.
src/pages/(app)/home.tsx/homeSigned-in home.
src/pages/(app)/(protected)/_layout.tsx-Wraps its children in <AuthGate>.
src/pages/(app)/(protected)/settings.tsx/settingsGated settings page.
src/pages/(app)/(protected)/api-status.tsx/api-statusGated platform-status page.

The three route tiers#

A page's folder decides its capabilities. The scaffold ships three tiers, and each carries a contract:

TierLocationContract
Staticsrc/pages/*.tsxNo providers - no auth fetch, no WebSocket. Data and auth hooks have nothing to read here and fail. The landing page lives in this tier and must keep that contract: / stays free of data and auth hooks so it renders instantly for signed-out visitors, crawlers, and tests.
Dynamic, signed-out capablesrc/pages/(app)/*DeepSpaceAuthProvider and RecordProvider are mounted, so every auth, data, and presence hook works - but there is no auth overlay. These pages render for signed-out visitors too; never assume a user exists.
Gatedsrc/pages/(app)/(protected)/*Everything above, plus <AuthGate>: sign-in is required before children render, so pages here may assume a signed-in user.

Put each page in the lowest tier that satisfies it: static if it needs no data, (app)/ if it needs data but should work signed out, (app)/(protected)/ only when the page is meaningless without a user.

Components#

FilePurpose
src/components/Navigation.tsxTop navigation with auth-aware controls. Reads entries from src/nav.ts.
src/components/ErrorScreen.tsxFull-screen error state used by the app boundary.
src/components/ui/Shadcn-style primitives: Button, Dialog, Modal, Toast, Tabs, Select, Popover, DropdownMenu, Tooltip, Avatar, Badge, Checkbox, Switch, Input, Textarea, Label, SearchInput, EmptyState. Barrel-exported from ui/index.ts.
src/lib/utils.tsThe cn() class-merging helper the UI primitives use.

Data layer#

FilePurpose
src/schemas.tsExports the array of every collection schema in the app.
src/schemas/users-schema.tsThe seeded users collection.
src/schemas/admin-schema.tsThe seeded settings collection.
src/actions/index.tsServer actions - privileged worker functions called via POST /api/actions/:name.
src/constants.tsAPP_NAME, SCOPE_ID, and role re-exports.

Schemas are imported by worker.ts and baked into the bundle at deploy time. There is no runtime schema registry - adding or changing a schema requires a redeploy.

Worker-side route modules#

worker.ts stays thin by delegating to these:

FilePurpose
src/server/http-routes.tsPlain HTTP routes on the app worker.
src/server/action-routes.tsWiring for src/actions/index.ts.
src/server/realtime-routes.tsThe /ws/* upgrade routes into the Durable Objects.

Theming and navigation#

FilePurpose
src/themes.tsTyped catalog of theme presets.
src/themes.cssPer-theme CSS variable blocks.
src/styles.cssTailwind v4 entrypoint with the baseline @theme block.
src/nav.tsTop-nav entries. Add new pages here to make them appear in Navigation.tsx.

Feature surfaces#

These ship pre-wired with empty defaults. Edit to populate, or delete the file if the feature isn't needed. Use npx deepspace add <feature> to install additional surfaces.

FilePurpose
src/cron.tsScheduled tasks for AppCronRoom. See Scheduled tasks.
src/jobs.tsBackground-job handlers for AppJobRoom. See Background jobs.
src/ai/tools.tsSystem prompt and tool allowlist for /api/ai/chat.
src/ai/chat-routes.tsHono handlers for the AI chat endpoints.
src/integrations.tsPer-integration billing config (developer vs user).
src/subscriptions.tsSubscription plan manifest for Stripe billing.
src/products.tsOne-time product manifest.

public/ - static assets#

FilePurpose
public/favicon.icoDefault favicon.
public/_headersCloudflare headers rules applied to static assets.

Large images and media should not live here or in Git - see deepspace app files.

tests/ - Playwright and Vitest specs#

FilePurpose
tests/smoke.spec.tsApp boot, navigation visibility, sign-in button presence, 404 route, console-error check.
tests/api.spec.tsAPI reachability and a WebSocket smoke check.
tests/collab.spec.tsTwo-user multi-context sign-in via the users fixture from deepspace/testing.
tests/helpers/errors.tsConsole-error capture helper.
tests/helpers/global-setup.tsPlaywright globalSetup that warms up the auth worker.
tests/playwright.config.tsbaseURL, webServer, DEEPSPACE_PORT plumbing.

Run with npx deepspace test run. See Testing.

App identity#

wrangler.toml carries two different names, and the distinction matters:

name = "my-app"                              # subdomain label - my-app.app.space

[vars]
DEEPSPACE_APP_ID = "app_01KZAA0A8YCS2..."    # immutable identity
APP_NAME = "my-app"
toml

DEEPSPACE_APP_ID never changes. Data, secrets, collaborators, releases, and the cloud repo all key to it, which is why renaming an app (changing name, then deploy --rename) is safe: the URL moves, everything else travels. deepspace app init mints the id; --new-id deliberately forks the repo into a separate app.

The Durable Object manifest#

worker.ts exports a __DO_MANIFEST__ constant listing the DO classes the app uses:

export const __DO_MANIFEST__ = [
  { binding: 'RECORD_ROOMS', className: 'AppRecordRoom', sqlite: true },
  { binding: 'YJS_ROOMS', className: 'AppYjsRoom', sqlite: true },
  { binding: 'CANVAS_ROOMS', className: 'AppCanvasRoom', sqlite: true },
  { binding: 'PRESENCE_ROOMS', className: 'AppPresenceRoom', sqlite: true },
  { binding: 'CRON_ROOMS', className: 'AppCronRoom', sqlite: true },
  { binding: 'JOB_ROOMS', className: 'AppJobRoom', sqlite: true },
] as const satisfies DOManifest
ts

Each entry has a binding (the env binding name your Hono routes look up), a className (the DO subclass exported from the same file), and sqlite (true for SQLite-backed DOs).

This constant exists for TypeScript inference. The Env interface picks up the binding names from it automatically:

interface Env extends DOBindings<typeof __DO_MANIFEST__> {
  // ...your secrets and custom bindings
}
ts

The deploy-time bindings and migrations are declared in wrangler.toml under [durable_objects] and [[migrations]]; keep the two in sync. Don't remove classes you no longer use without clearing their stored data first - records persist across deploys, and rollback refuses to drop a class without --allow-do-deletion.

Next steps#