Skip to main content

Self-hosting

A Norn instance is three long-running processes and three backing services. Everything below is what the software actually requires — there is no separate self-hosted edition.

What you need

ComponentPurpose
PostgreSQLPrimary datastore. Migrations are embedded in the binary.
ValkeySessions, caching, rate limits, the job queue, and Casbin policy synchronisation.
S3-compatible object storageAttachments and import payloads.
SMTP relayVerification, password reset, and invitation mail.

Valkey must run with noeviction. Cache misses are tolerated, but sessions, rate limits, jobs, and policy synchronisation fail closed — silently evicting those keys will lock people out rather than degrade.

warning

The docker-compose.yml in the repository is a local development stack only, never a deployment artifact. It exists so make dev can bring up PostgreSQL, Valkey, and Garage on a laptop. Do not deploy from it.

The processes

norn migrate # apply embedded migrations, then exit
norn seed # idempotently seed the Casbin policy and development data
norn serve # the HTTP API on :8080
norn worker # background jobs
node build # the SvelteKit dashboard

norn migrate runs the migrations embedded in the binary, so there is no separate migration image or goose CLI to install. Run it before serve on every upgrade.

norn seed reconciles the Casbin authorisation policy. A freshly migrated database has no policy rows, and serve does not reconcile them on boot — every protected route will return 403 until the seed has run at least once.

norn jobs is the operator surface for the queue. Archived tasks are the dead-letter queue.

Configuration

Configuration precedence is environment variables (prefixed NORN_) over an optional --config file over built-in defaults. Nested keys flatten with underscores, so postgres.dsn is NORN_POSTGRES_DSN.

.env
NORN_APP_ENV=production
NORN_APP_LOG_LEVEL=info
NORN_APP_BASE_URL=https://norn.example.com
NORN_POSTGRES_DSN=postgres://norn:secret@127.0.0.1:5432/norn?sslmode=require
NORN_VALKEY_ADDR=127.0.0.1:6379
NORN_ASYNQ_ADDR=127.0.0.1:6379
NORN_SECURITY_ENCRYPTION_KEY=

The reverse proxy contract

The Node server and /v1 must share one origin. The session cookie is host-only (Domain="", SameSite=Lax) and the Go server ships no CORS middleware, so a proxy putting both on one hostname is a requirement, not a preference.

/v1 is never routed through SvelteKit. Kit's CSRF origin check would reject the SAML ACS cross-site POST, and an SSE stream or a multi-megabyte upload has no business on the Node event loop.

Node

VariableValueWhy
ORIGINhttps://norn.example.comWithout it, adapter-node computes the wrong URL and Kit's CSRF check returns 403 on every form action — in production only, since the check is disabled in dev.
ADDRESS_HEADERx-forwarded-forSo Kit reads the real client address.
XFF_DEPTHnumber of proxies in frontMust match reality.
BODY_SIZE_LIMITat least NORN_HTTP_MAX_REQUEST_BYTESadapter-node defaults to 512 KiB against Go's 4 MiB.
INTERNAL_API_ORIGINhttp://127.0.0.1:8080Keeps server-side loads off the public proxy.

Go

NORN_HTTP_CLIENT_IP_HEADER=X-Forwarded-For
NORN_HTTP_TRUSTED_PROXIES=10.0.0.0/8

Set these two together. The server refuses to boot otherwise, because trusting the header without an allow-list makes the per-address sign-in throttle forgeable.

Proxy

  • Enable gzip or brotli on text/html and application/json. adapter-node precompresses static assets but never compresses SSR output.
  • For /v1/workspaces/*/events, set proxy_read_timeout 0 and turn buffering off — it is a Server-Sent Events stream.
  • Set client_max_body_size to at least the attachment limit.
nginx.conf
location /v1/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 25m;
}

location /v1/workspaces/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 0;
}

location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
danger

NORN_SECURITY_ENCRYPTION_KEY protects stored credentials such as SSO client secrets. Generate it once, back it up, and never rotate it casually — losing it means losing the ability to decrypt everything that was encrypted under it.

Upgrading

  1. Take a database backup.
  2. Deploy the new binary.
  3. Run norn migrate.
  4. Run norn seed so any newly introduced resource gets its policy rows.
  5. Restart norn serve and norn worker.