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#
| File | Purpose |
|---|---|
worker.ts | Hono worker and Durable Object class declarations. Edit to add custom routes or DO classes. |
wrangler.toml | Cloudflare config. Holds the app's immutable DEEPSPACE_APP_ID under [vars], the name (subdomain label), DO bindings and migrations, and any custom bindings. |
index.html | HTML shell. Set <html data-theme="..."> to pick a theme, update <title> and favicon. |
vite.config.ts | Vite + @cloudflare/vite-plugin + generouted config. |
vitest.config.ts | Vitest config for the unit suite. |
eslint.config.js | Flat ESLint config. |
postcss.config.js | PostCSS pipeline for Tailwind v4. |
tsconfig.json | TypeScript config. Strict mode, covers src/ and worker.ts. |
package.json | Dependencies and the deepspace-backed npm scripts (dev, build, test, deploy). |
deepspace.migrations.json | App-owned ledger of which platform migration guides this project has applied and validated. app update reads it but never writes it. |
skills-lock.json | Pins the revision of the agent skill installed under .agents/. |
AGENTS.md | Agent-facing project brief, read by tools that follow the AGENTS.md convention. |
CLAUDE.md | Claude Code instructions - points at the bundled DeepSpace skill. |
.gitignore | Ignores node_modules, dist, .wrangler, .dev.vars*, .deepspace, and the generated src/router.ts / src/modals.tsx. |
.dev.vars | Local secrets (gitignored). Not in the template - deepspace dev start writes SDK-managed keys here on first run. |
Agent tooling#
| Path | Purpose |
|---|---|
.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.json | Claude 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.
| File | Route | Purpose |
|---|---|---|
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 DeepSpaceAuthProvider → AuthBoot → RecordProvider → RecordScope, plus the Navigation chrome. |
src/pages/(app)/home.tsx | /home | Signed-in home. |
src/pages/(app)/(protected)/_layout.tsx | - | Wraps its children in <AuthGate>. |
src/pages/(app)/(protected)/settings.tsx | /settings | Gated settings page. |
src/pages/(app)/(protected)/api-status.tsx | /api-status | Gated platform-status page. |
The three route tiers#
A page's folder decides its capabilities. The scaffold ships three tiers, and each carries a contract:
| Tier | Location | Contract |
|---|---|---|
| Static | src/pages/*.tsx | No 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 capable | src/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. |
| Gated | src/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#
| File | Purpose |
|---|---|
src/components/Navigation.tsx | Top navigation with auth-aware controls. Reads entries from src/nav.ts. |
src/components/ErrorScreen.tsx | Full-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.ts | The cn() class-merging helper the UI primitives use. |
Data layer#
| File | Purpose |
|---|---|
src/schemas.ts | Exports the array of every collection schema in the app. |
src/schemas/users-schema.ts | The seeded users collection. |
src/schemas/admin-schema.ts | The seeded settings collection. |
src/actions/index.ts | Server actions - privileged worker functions called via POST /api/actions/:name. |
src/constants.ts | APP_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:
| File | Purpose |
|---|---|
src/server/http-routes.ts | Plain HTTP routes on the app worker. |
src/server/action-routes.ts | Wiring for src/actions/index.ts. |
src/server/realtime-routes.ts | The /ws/* upgrade routes into the Durable Objects. |
Theming and navigation#
| File | Purpose |
|---|---|
src/themes.ts | Typed catalog of theme presets. |
src/themes.css | Per-theme CSS variable blocks. |
src/styles.css | Tailwind v4 entrypoint with the baseline @theme block. |
src/nav.ts | Top-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.
| File | Purpose |
|---|---|
src/cron.ts | Scheduled tasks for AppCronRoom. See Scheduled tasks. |
src/jobs.ts | Background-job handlers for AppJobRoom. See Background jobs. |
src/ai/tools.ts | System prompt and tool allowlist for /api/ai/chat. |
src/ai/chat-routes.ts | Hono handlers for the AI chat endpoints. |
src/integrations.ts | Per-integration billing config (developer vs user). |
src/subscriptions.ts | Subscription plan manifest for Stripe billing. |
src/products.ts | One-time product manifest. |
public/ - static assets#
| File | Purpose |
|---|---|
public/favicon.ico | Default favicon. |
public/_headers | Cloudflare 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#
| File | Purpose |
|---|---|
tests/smoke.spec.ts | App boot, navigation visibility, sign-in button presence, 404 route, console-error check. |
tests/api.spec.ts | API reachability and a WebSocket smoke check. |
tests/collab.spec.ts | Two-user multi-context sign-in via the users fixture from deepspace/testing. |
tests/helpers/errors.ts | Console-error capture helper. |
tests/helpers/global-setup.ts | Playwright globalSetup that warms up the auth worker. |
tests/playwright.config.ts | baseURL, 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"
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
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
}
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#
- Quickstart - build something with these files.
- Architecture - how everything fits together.
- Data model - collections, records, and the envelope shape.