Quickstart (full stack in Docker)
The fastest path to a running server with seeded data and a working API key. The compose stack builds the server fromapps/server/Dockerfile; a one-shot migrations init service (behind the init profile) migrates, provisions the runtime role, and sets its password before the server starts.
- The
migrationsservice and theserverservice both build fromapps/server/Dockerfile(themigrationsbuild target addspsql);serverexposes 3000:3000 with a Node-fetch/healthprobe. pnpm seedruns on the host (not in a container) because it loads the committed eval fixtures; it connects to Postgres on the published port 54320. The demo API key (nameself-host demo) is minted once and is idempotent — re-running prints “already present” rather than re-issuing.APP_USER_PASSWORDdefaults toapp-user-dev(compose) — override it in.envfor anything beyond a throwaway local stack. Because the host-runpnpm seedreads the literalDATABASE_URL(it does not derive the password fromAPP_USER_PASSWORDthe way compose does for theserverservice), change the password in bothAPP_USER_PASSWORDand the password segment ofDATABASE_URL— overriding one alone desyncs the two andpnpm seedfails with an auth error.- A worker container is not in compose yet (no
apps/worker/Dockerfile); BullMQ jobs need a separate deployable before they are part of the self-host stack.
Prerequisites
- Docker (compose v2), Node ≥ 22, pnpm 10 (
corepack enable) psqlclient
1. Start the services
2. Configure the environment
Copy.env.example to .env and set the database keys. Then load it into your shell — nothing in the repo auto-loads .env (no dotenv dependency, by design), and the migrate/seed/psql commands below read the variables from the environment:
| Variable | Value (local) | Role |
|---|---|---|
DATABASE_URL_UNPOOLED | postgresql://postgres:3ngram-dev@localhost:54320/ngram | Owner — migrations, role provisioning, seed user creation. Bypasses RLS; never used by app code. |
DATABASE_URL | postgresql://app_user:<password>@localhost:54320/ngram | Runtime — what the app and integration tests use. app_user has NOBYPASSRLS and append-only grants. |
APP_USER_PASSWORD | the password you pick in step 3 | |
SENTRY_DSN | (empty) | Empty = error tracking and tracing fully disabled — the self-host default; no phone-home. Set it to your own Sentry DSN to send errors to your instance; optionally set SENTRY_ENVIRONMENT to tag events (defaults to NODE_ENV). |
3. Migrate
4. Provision roles
After migrating — the grants reference tables the migrations create (same order as CI).provision-roles.sql is idempotent and is the only place grants live. Note what it deliberately does NOT grant the runtime role: DELETE anywhere in the memory domain, and anything but INSERT/SELECT on memory_events and audit_log — append-only is enforced at the grant level, below the application.
5. Seed (optional)
dev@localhost, override with SEED_EMAIL). The seed writes through the runtime role inside a set_config('app.user_id', …) transaction — the same RLS gate the app uses — and is idempotent the append-only way: existing rows (matched by content_hash) are skipped, never deleted. Embeddings stay NULL; nothing calls an external API.
Sanity check:
6. Run the test suites against it
Resetting
What’s deliberately not here yet
- worker / web containers — the server container exists; worker and dashboard compose support land when those deployables are packaged for self-host.
- Embedding-backed search — seeded memories have
NULLembeddings; vector search comes alive once the write path embeds on write.