Quickstart
Scaffold, run, and deploy a real-time app.
On this page
This guide takes you from npm create to a deployed app at <your-app>.app.space. You'll need a supported Node line (see installation) and a GitHub or Google account for sign-in.
For prerequisites and account setup, see installation. For a tour of the files the scaffolder generates, see project structure.
1. Scaffold an app#
The create-deepspace package bootstraps a new project with the SDK preinstalled, a worker wired to a Durable Object, and a Vite-powered React frontend.
npm create deepspace@latest my-app
cd my-app
The scaffolder is non-interactive by default and prints a list of files it created. Pass --interactive if you want a prompt-driven wizard.
If you are already signed in, the last thing the scaffolder does is register the app under that account - it names the account and the plane as it goes. See which account the app registers under if more than one login could be active on this machine, or if you want to scaffold now and register later with --no-register.
2. Start the dev server#
npx deepspace dev start
This runs Vite and the worker together and opens http://localhost:5173 with hot-module reload. The CLI also writes an app-owner JWT and platform URLs to .dev.vars, so your local worker can call the live auth, storage, and integration services.
Pass --port to run multiple apps in parallel:
npx deepspace dev start --port 5180
3. Sign in to the CLI#
You'll need a DeepSpace account to deploy. One-time setup:
npx deepspace auth login
This opens your browser for GitHub or Google OAuth and stores a long-lived session at ~/.deepspace/session. DeepSpace accounts are OAuth-only - there is no password. The same session covers every app on the machine, and every later command refreshes from it on its own.
Check status anytime with:
npx deepspace auth whoami
Manage your account, deployed apps, and billing at dashboard.deep.space.
4. Make it yours#
Open the project in your editor.
Key files:
src/pages/index.tsx- the static landing page at/. Edit the hero copy.src/pages/(app)/home.tsx- the signed-in home route at/home.src/schemas.ts- collection schemas. Add a new collection alongside the seededusersandsettings.src/pages/(app)/_layout.tsx- the provider stack. Auth and realtime data are already wired.worker.ts- the Hono worker. Add custom routes here.index.html- change the<title>anddata-themeattribute to pick a preset.
Where a page goes decides what it can do#
Routing is file-based, and the folder a page sits in determines which providers wrap it. There are three tiers:
| Location | Providers | Use for |
|---|---|---|
src/pages/*.tsx | None | Static pages - marketing, legal, docs. No auth fetch, no WebSocket. |
src/pages/(app)/*.tsx | DeepSpaceAuthProvider + RecordProvider | Dynamic pages. useAuth, useQuery, useMutations, presence all work. Sign-in not required. |
src/pages/(app)/(protected)/*.tsx | The above, plus <AuthGate> | Pages that require sign-in. |
Folders in parentheses are generouted route groups - they apply a layout without appearing in the URL. So (app)/(protected)/todos.tsx serves at /todos.
Try adding a todos collection. For the full schema reference, see worker schemas.
// src/schemas/todos-schema.ts
import type { CollectionSchema } from 'deepspace/worker'
export const todosSchema: CollectionSchema = {
name: 'todos',
columns: [
{ name: 'title', storage: 'text', interpretation: 'plain' },
{ name: 'completed', storage: 'number', interpretation: { kind: 'boolean' } },
],
permissions: {
member: { read: true, create: true, update: 'own', delete: 'own' },
admin: { read: true, create: true, update: true, delete: true },
},
}
// src/schemas.ts
import type { CollectionSchema } from 'deepspace/worker'
import { todosSchema } from './schemas/todos-schema'
import { usersSchema } from './schemas/users-schema'
import { settingsSchema } from './schemas/admin-schema'
export const schemas: CollectionSchema[] = [usersSchema, settingsSchema, todosSchema]
Then read and write from the UI using useQuery and useMutations:
// src/pages/(app)/(protected)/todos.tsx
import { useQuery, useMutations } from 'deepspace'
type Todo = { title: string; completed: boolean }
export default function TodosPage() {
const { records, status } = useQuery<Todo>('todos', { orderBy: 'createdAt' })
const { create, put, remove } = useMutations<Todo>('todos')
if (status === 'loading') return <p>Loading…</p>
return (
<>
<ul>
{records.map((r) => (
<li key={r.recordId}>
<input
type="checkbox"
checked={r.data.completed}
onChange={(e) => put(r.recordId, { completed: e.target.checked })}
/>
{r.data.title}
<button onClick={() => remove(r.recordId)}>Delete</button>
</li>
))}
</ul>
<button onClick={() => create({ title: 'New todo', completed: false })}>
Add
</button>
</>
)
}
The page lives at /todos - generouted picks it up automatically. Because it sits under (app)/(protected)/, it inherits <RecordProvider> from (app)/_layout.tsx (so the hooks above work) and <AuthGate> from (app)/(protected)/_layout.tsx (so it requires sign-in). See authentication. Save and the page hot-reloads.
5. Deploy#
Commit first - a deploy records the commit it ships:
git add -A && git commit -m "add todos"
npx deepspace deploy
The CLI bundles the worker, uploads it to Cloudflare Workers for Platforms, syncs any secrets from .dev.vars, and registers your subdomain. The deployment reference covers the full pipeline. Within a few seconds, your app is live at:
https://<your-app>.app.space
Visit the URL, sign in, and add a todo. Open the same URL in a second tab - edits sync between them instantly.
Next steps#
You have a working app. Where to go from here:
- Authentication - pick a public, gated, or mixed model.
- Collaborators - let a teammate deploy this app.
- Presence and cursors - show who's online and broadcast cursor positions.
- Payments - subscriptions and one-time products via Stripe.
- AI chat - streamed Claude or GPT with tool use over your records.