# Sited Docs Sited is a personal, agent-native deploy runtime for small full-stack TypeScript apps called capsules. If you are an agent building for Sited, treat the capsule directory as the whole app. Define the server contract, write the Preact client, run the Sited CLI, inspect runtime state when needed, and deploy without adding per-app Docker, Vite, or server infrastructure. Machine-readable docs are available at: https://d.ellep.dev/docs.json ## Start Here A healthy Sited capsule has this shape: server/index.ts server/ client/index.tsx client/components/ client/hooks/ client/lib/ client/style.css shared/ assets/ data/ .sitedignore - `server/index.ts`: required server entrypoint. Default-export `app(...)` and import from `sited/server`. - `server/`: optional server-only helpers imported by `server/index.ts`. - `client/index.tsx`: required Preact UI entrypoint. Export `App`; keep larger UIs split into normal files under `client/`. - `client/components/`, `client/hooks/`, `client/lib/`: optional browser-only UI modules. - `client/style.css`: optional Tailwind v4 CSS-first customization. Most styling should be Tailwind classes in JSX. - `shared/`: pure TypeScript shared by server and client. Do not read env, secrets, DOM APIs, Node APIs, or Sited runtime APIs here. - `assets/`: browser-facing static files served from `/assets/...`. - `data/`: bundled static data served from `/data/...` and importable from server/shared code. - `.sitedignore`: optional deploy packaging ignores for files under `assets/` and `data/`. Only the entrypoint paths are fixed. Use normal module boundaries for the rest of the app; do not turn `client/index.tsx` or `server/index.ts` into a dumping ground once the app grows. Sited owns the HTML shell, client bundle loading, `/style.css` link, Tailwind compilation, RPC transport, private password gate, deployment wrapper, and runtime routing. The capsule owns its server contract, Preact UI, shared domain logic, static files, package scripts, and tests. For a new capsule: npx @lucaspll/sited new SLUG cd SLUG npm install For an existing capsule, install the dev dependency with the `sited` package alias: npm init -y npm install --save-dev sited@npm:@lucaspll/sited Use the alias. Capsule source imports `sited/server` and `sited/client`; installing `@lucaspll/sited` directly will not satisfy those imports for TypeScript and editors. Recommended package scripts use the local `sited` binary installed by the alias: ```json { "scripts": { "dev": "sited dev . --slug SLUG", "build": "npm run typecheck && sited build .", "typecheck": "tsc --noEmit" }, "devDependencies": { "sited": "npm:@lucaspll/sited" } } ``` Use `npx @lucaspll/sited ...` for one-off commands before install, from outside a capsule, or in automation that intentionally does not rely on the capsule lockfile. Inside checked-in npm scripts, prefer `sited ...` so the CLI version matches the project dependency. Common one-off commands: npx @lucaspll/sited build . npx @lucaspll/sited dev . --slug SLUG SITED_API=https://d.ellep.dev npx @lucaspll/sited deploy . --slug SLUG For private apps, include the app password on first deploy: SITED_API=https://d.ellep.dev npx @lucaspll/sited deploy . --slug SLUG --password "$APP_PASSWORD" The owner must provide `SITED_ADMIN_TOKEN` in the environment or pass `--admin-token`. Never write the token into source files, logs, client code, or committed config. Deployed app URL: https://SLUG.d.ellep.dev ## Server Contract Every capsule default-exports `app(...)` from `server/index.ts`. ```ts import { app, endpoint, jsonResponse, mutation, query, string, table } from "sited/server"; const appDefinition = app({ name: "Example", access: "public", schema: { notes: table({ body: string(), }), }, queries: { notes: query((ctx) => ctx.db.notes.orderBy("createdAt", "desc").all()), note: query((ctx, id: string) => ctx.db.notes.get(id)), }, mutations: { addNote: mutation((ctx, body: string) => ctx.db.notes.insert({ body })), updateNote: mutation((ctx, id: string, body: string) => ctx.db.notes.update(id, { body })), deleteNote: mutation((ctx, id: string) => ctx.db.notes.delete(id)), }, endpoints: { health: endpoint({ method: "GET", path: "/api/health" }, () => jsonResponse({ ok: true })), }, }); export default appDefinition; ``` Server handlers receive: - `ctx.auth`: current guest or private-app identity. - `ctx.db`: table access for the capsule database. - `ctx.env`: server-only environment values. - `ctx.fetch`: server-side fetch. - `ctx.log`: structured logs captured by the runtime. Handler kinds: - `query`: read data. - `mutation`: write database data. - `action`: run external side effects or non-database work. - `endpoint`: expose custom HTTP routes and webhooks. Server rules: - Treat queries and mutations as the source of truth. - Validate inputs in server handlers before writing. - Re-check ownership inside mutations before updates or deletes. - Use `ctx.auth.userId` for user-owned data. - Read secrets only from `ctx.env`. - Return structured validation results when the client should handle an expected error. Throwing returns a server error response and writes to app logs. Available server imports: - `app` - `query` - `mutation` - `action` - `endpoint` - `table` - `string` - `number` - `boolean` - `json` - `jsonResponse` - `text` - `empty` - `redirect` ## Client Contract The client exports `App` from `client/index.tsx`. ```tsx import { createClient, useMutation, useQuery } from "sited/client"; import type appDefinition from "../server/index"; type Note = { id: string; body: string; createdAt: string; updatedAt: string; }; const api = createClient(); export function App() { const notes = useQuery("notes"); const addNote = useMutation<[body: string], Note>("addNote"); return (
{JSON.stringify(notes.data ?? [], null, 2)}
); } ``` Use Preact and browser APIs in client code. Do not import from `react` or `react-dom` in capsules. Use `preact`, `preact/hooks`, and Preact JSX or `ComponentChildren` types when needed. Available client imports: - `createClient()` returns typed `query`, `mutation`, and `action` RPC groups. - `useQuery(name, args?)` returns `{ data, error, isLoading, refetch }`. - `useMutation(name)` returns `{ error, isLoading, mutate }`. - `useAction(name)` returns `{ error, isLoading, run }`. - `useAuth()` returns `{ displayName, isAuthenticated, isGuest, isLoading, provider, userId }`. - `refreshAuth()` - `signInWithPassword(password)` - `signOut()` Hook arguments are positional: ```ts const api = createClient(); const typedNote = await api.query.note(noteId); const typedUpdate = await api.mutation.updateNote(noteId, "new body"); const note = useQuery("note", [noteId]); const updateNote = useMutation<[id: string, body: string], Note>("updateNote"); ``` For private apps, Sited serves a built-in password gate before the capsule client loads. Custom private-app UI can call `signInWithPassword(password)` and `signOut()`. ## Styling Use Tailwind classes directly in JSX. Keep classes statically visible to the build: ```tsx const tones = { danger: "bg-red-500 text-white", success: "bg-emerald-500 text-white", }; ; ``` Do not generate class names with template strings such as `bg-${color}-500`. Use `client/style.css` only for Tailwind v4 CSS-first customization: ```css @theme { --color-brand: #0e7490; } @layer base { body { @apply antialiased; } } ``` Sited owns the stylesheet pipeline. Do not install Tailwind in capsules, add PostCSS, add Vite, create `tailwind.config.js`, add Tailwind CLI scripts, commit generated CSS, import `client/style.css` from `client/index.tsx`, or add manual stylesheet links. Sited compiles and links `/style.css` automatically. External CSS imports in `client/style.css` should be deliberate. If you use hosted fonts, keep the font import and theme variables together. If you remove a font import, also update font-family variables and fallback stacks so the UI does not reference fonts that are no longer loaded. ## Auth, Env, Data `access` can be `"public"` or `"private"`. - `public`: serves without login. - `private`: requires a password and creates the app owner actor. First deploy of a private app must include `--password`. Public apps must not be deployed with `--password`. Server handlers read auth from `ctx.auth`: ```ts { displayName: string; isAuthenticated: boolean; isGuest: boolean; provider: "guest" | "private"; userId: string; } ``` Set server-only environment values after deploy: SITED_API=https://d.ellep.dev npx @lucaspll/sited env set SLUG OPENAI_API_KEY "$OPENAI_API_KEY" printf %s "$OPENAI_API_KEY" | SITED_API=https://d.ellep.dev npx @lucaspll/sited env set SLUG OPENAI_API_KEY --stdin List or remove environment keys: SITED_API=https://d.ellep.dev npx @lucaspll/sited env list SLUG SITED_API=https://d.ellep.dev npx @lucaspll/sited env unset SLUG OPENAI_API_KEY Read them only from server handlers: ```ts ctx.env.OPENAI_API_KEY; ``` Do not expose server secrets through browser-bundled environment variables, client code, shared code, logs, or committed config. Table scopes: - `table(fields)`: app-wide rows. - `table(fields, { scope: "owner" })`: rows automatically scoped to `ctx.auth.userId`; requires an authenticated user. Table methods: - `insert(value)` - `get(id)` - `update(id, value)` - `delete(id)` - `all()` - `where(field, value)` - `orderBy(field, "asc" | "desc")` - `limit(count)` Static files: - `assets/` files are served from `/assets/...`. - `data/` files are served from `/data/...`. - `.sitedignore` excludes files from the packaged `assets/` and `data/` trees before deploy. It supports blank lines, `#` comments, `!` negation, `/`-anchored paths relative to the capsule root, directory patterns such as `assets/raw/`, and `*`, `?`, `**` globs. - Private apps gate HTML, client assets, data files, RPC routes, actions, and endpoints. - Keep `assets/` lean. Only include files the app actually references or intentionally serves. Use direct `/data/...` fetches for bundled static data that is already safe to expose to the browser. Use `query` when the server needs to read the database, enforce auth, use secrets, compute or filter results on demand, hide raw source data, or return user-specific values. Do not add an RPC handler just to pass through unchanged public JSON. There is no general runtime object-upload API. Use `assets/` and `data/` for bundled static files. For small private uploads, store compact metadata or small data URLs in `json` fields only after server-side validation and size limits. For mixed public/private workflows in one capsule, use `access: "public"` and implement app-level protection inside server handlers. Sited's built-in password gate applies to the whole capsule. ## Inspect Runtime State Use inspection commands before guessing: SITED_API=https://d.ellep.dev npx @lucaspll/sited logs SLUG SITED_API=https://d.ellep.dev npx @lucaspll/sited db SLUG SITED_API=https://d.ellep.dev npx @lucaspll/sited assets SLUG For local inspection: SITED_API=http://127.0.0.1:7331 SITED_ADMIN_TOKEN=local-dev-token npx @lucaspll/sited db SLUG Delete a deployed app: SITED_API=https://d.ellep.dev npx @lucaspll/sited delete SLUG --yes Deletion removes routing, deploy metadata, sessions, secrets, logs, app database, and the app server bundle directory. ## Current Limits - One server entry: `server/index.ts`. Supporting server modules under `server/` are fine. - One client entry: `client/index.tsx`. Supporting UI modules under `client/` are fine. - App code should use relative imports, `sited/server`, `sited/client`, Preact, browser APIs in client code, and bundled pure TypeScript dependencies. - Native Node modules inside capsules are unsupported. - Runtime dynamic imports inside capsules are unsupported. - Capsule-level Tailwind config, PostCSS config, Vite config, and Tailwind plugins are unsupported. - Each deploy asset is limited to 10 MiB before base64 encoding. - The total raw deploy blob payload is limited to 16 MiB before base64 encoding. - Capsule server code is trusted and runs in the Sited runtime process. Sited isolates app data by app id but does not sandbox malicious app code from the host runtime or other in-process apps. - No per-app Docker containers. - Rollback means reverting source and deploying again.