OpenSequence
DocsAPI referenceSDKs and CLIReference
Self-hosting

Install

docker compose, the image, first run, the operator token, creating a project.

Edit on GitHub
git clone https://github.com/lguerberg/open-sequence && cd open-sequence
docker compose up -d
curl http://localhost:8080/readyz        # {"ok":true}
curl -X POST http://localhost:8080/v1/admin/projects -H "Authorization: Bearer dev-admin-token" -H "Content-Type: application/json" \
  -d '{"slug":"acme","name":"Acme CRM"}'   # -> the project and its first admin key (shown once)

The compose file runs Postgres 16 and one OpenSequence container with SEQ_ROLES=all, SEQ_DEV_MOCK_PROVIDERS=1, migrations on boot, a fixed development encryption key and the operator token dev-admin-token. It is the development setup; production changes four things: real secrets, mock providers off, provider credentials registered, and (optionally) the roles split.

The image

opensequence/server is built from apps/server/Dockerfile (docker build -f apps/server/Dockerfile -t opensequence/server .): Node 22 on Alpine, a non-root user, node dist/main.js, port 8080, a HEALTHCHECK on /healthz. One image runs every role; SEQ_ROLES picks one. The oseq CLI is inside at dist/cli.js.

Provision Postgres

Postgres 14 or newer, no extensions. A transaction pooler (PgBouncer, Supabase's pooler) is fine: the service uses no session-level features. Give the service its own database or at least its own schemas: it creates seq and auth.

Generate the secrets

docker run --rm opensequence/server node dist/cli.js secrets new-key   # -> k1abc:…base64…   for SEQ_ENCRYPTION_KEYS
openssl rand -base64 32                                                # for SEQ_ADMIN_TOKEN

SEQ_ENCRYPTION_KEYS encrypts every stored secret (Google client secret, refresh tokens, Allo and Attio keys, webhook secrets). Back it up with the database: without it those rows are unreadable and every credential must be re-entered.

Run the api role

docker run -d --name oseq-api -p 8080:8080 \
  -e SEQ_DB_DSN=postgres://oseq:…@db.internal:5432/opensequence \
  -e SEQ_ENCRYPTION_KEYS=k1abc:… -e SEQ_ADMIN_TOKEN=… \
  -e SEQ_PUBLIC_URL=https://sequences.example.com \
  -e SEQ_ROLES=api -e SEQ_RUN_MIGRATIONS=1 -e NODE_ENV=production \
  opensequence/server

Migrations run at boot behind a row lock, so several replicas starting together do not race. SEQ_PUBLIC_URL is what providers call back and what the opt-out links point at: it must be reachable from the internet over HTTPS (Google's OAuth return, Allo's webhook, the links in your emails). Put a TLS terminator in front; the service speaks plain HTTP.

Run the worker role

The same image with SEQ_ROLES=worker, not exposed. It listens on SEQ_PORT for /healthz and /readyz only. For a small install, SEQ_ROLES=all in one container is fine.

Create the first project

docker exec oseq-api node dist/cli.js projects create acme --name "Acme CRM"
# or
curl -X POST https://sequences.example.com/v1/admin/projects -H "Authorization: Bearer $SEQ_ADMIN_TOKEN" -H "Content-Type: application/json" -d '{"slug":"acme","name":"Acme CRM"}'

The response carries the project id and its first admin key. Create narrower keys for your application with POST /v1/api-keys ({ name, scopes: ["enroll"] }) and keep the admin key for configuration.

Register the providers

With SEQ_DEV_MOCK_PROVIDERS off, every project needs its own Google OAuth client before senders can connect, an Allo key for texts and calls, and an Attio key for crm.* conditions. GET /v1/project shows the webhook URLs to expect (/hooks/allo/{slug}, /hooks/google/{slug}) and the Google redirect URI.

Health

PathAnswers
/healthz{ ok: true } while the process runs (liveness)
/readyz503 until the database answers and migrations are at the expected version; for worker and all also 503 when the scheduler has not run a job in the last 2 minutes
GET /v1/admin/engine/healthqueue depths per queue and the last run of every job, platform-wide, with the operator token
GET /v1/engine/healththe same for one project plus its flags, with an admin key

Logs are Pino JSON on stdout (pretty in development); every line about an enrollment carries its ids. See Observability, Configuration for every variable, and Security hardening before you expose it.

On this page