Skip to main content
How to run 3ngram locally from a clean clone. The same path serves local development, integration tests, and the current self-host skeleton. For the packaged adopter path (hardened Compose stack, secrets preflight), see Self-host. Everything below works on stock Postgres: the schema, RLS policies, and role grants have no cloud-specific dependencies (validated by the integration suites).

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 from apps/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.
Notes:
  • The migrations service and the server service both build from apps/server/Dockerfile (the migrations build target adds psql); server exposes 3000:3000 with a Node-fetch /health probe.
  • pnpm seed runs 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 (name self-host demo) is minted once and is idempotent — re-running prints “already present” rather than re-issuing.
  • APP_USER_PASSWORD defaults to app-user-dev (compose) — override it in .env for anything beyond a throwaway local stack. Because the host-run pnpm seed reads the literal DATABASE_URL (it does not derive the password from APP_USER_PASSWORD the way compose does for the server service), change the password in both APP_USER_PASSWORD and the password segment of DATABASE_URL — overriding one alone desyncs the two and pnpm seed fails 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 11.9.0 (corepack enable uses the version pinned by the repository; on Node ≥ 25, which dropped corepack, run npm install -g pnpm@11.9.0 instead)
  • psql client

1. Start the services

Postgres (pgvector, PG18) listens on 54320, Redis on 63790 — non-default ports so they never collide with system services. Images are pinned by digest; the tag each digest was resolved from is in the compose comments.

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:

3. Migrate

Migrations are forward-only; re-running is a no-op.

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)

Loads the anonymized eval golden set (158 memories including supersession chains) under a dev user (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. Cached fixture vectors are loaded for seeded memories; the seed itself never calls an external API. Sanity check:

6. Run the test suites against it

Resetting

Then repeat from step 1. (Dropping the volume is the supported “delete” path; runtime-role deletes don’t exist by design.)

Deployment boundaries and gaps

  • worker container — the server container exists; the worker needs a separate deployable before it is part of the self-host stack.
  • hosted dashboard — the dashboard UI is proprietary and maintained in a separate private repository; the Apache server includes the REST/auth routes clients need, but this repository does not ship that UI.
  • embedding provider — seeded memories have cached vectors, but every search query and new write still needs an embedding. Set both LLM_GATEWAY_URL and LLM_GATEWAY_API_KEY; without them, search returns 503 embedding_unavailable while non-search reads and writes remain available.