Self-Hosting
Architecture overview and production deployment guide
Architecture
A LinkedRecords deployment consists of the following components:
- LinkedRecords server - stateless Node.js process serving the HTTP API and Socket.IO real-time connections.
- PostgreSQL (required) - stores facts (the triplestore that drives authorization) and record values.
- OIDC provider (required) - any OpenID Connect compliant identity provider. LinkedRecords never stores passwords.
- Redis (optional) - only needed when running multiple server instances, so real-time events reach clients connected to other instances.
- S3-compatible object storage (optional, recommended) - stores blob record values; without it they go into PostgreSQL.
Running the Server
Docker (recommended)
A multi-arch image is published to GitHub Container Registry:
See the configuration reference for all environment variables and the difference between confidential and public client mode.
From Source
Database Setup
Point the server at an empty PostgreSQL database via the PG* variables.
The schema is created automatically on server start (CREATE TABLE IF NOT EXISTS ...) - there is no separate migration step to run.
For backups, standard PostgreSQL tooling (pg_dump, WAL archiving, managed
provider snapshots) covers all data - unless you configured S3, in which case
blob values live in the bucket and must be backed up separately.
TLS
Most deployments should terminate TLS at a reverse proxy, load balancer, or ingress and run the server with plain HTTP behind it. Make sure the proxy supports WebSocket upgrades (for Socket.IO real-time connections).
Alternatively, the Node.js server can terminate TLS itself: set HTTPS=true
and provide SSL_KEY and SSL_CRT (PEM contents).
Scaling to Multiple Instances
A single instance is sufficient for most workloads (see the performance chart in the README). To run multiple instances behind a load balancer:
- Set
REDIS_HOST(andREDIS_USERNAME/REDIS_PASSWORD) on every instance so real-time events are fanned out across instances via Redis Streams. - Consider setting
SHORT_LIVED_ACCESS_TOKEN_SIGNINGto a shared secret to reduce database load from subscription authorization checks. - Sticky sessions are recommended for Socket.IO long-polling fallback.
Monitoring
- Liveness probe:
GET /oidc/discoveryresponds without authentication. - Logs: structured JSON on stdout (pino); request and response headers are redacted.
- Rate limiting: built-in limit of 1000 requests/second per IP.
Production Checklist
- PostgreSQL provisioned with backups;
PG*variables set - Real OIDC provider configured -
AUTH_DEV_MODEnot set -
FRONTEND_BASE_URL,SERVER_BASE_URL, andCORS_ORIGINmatch your domains exactly - Strong random secrets for
AUTH_COOKIE_SIGNING_SECRET(confidential mode) andSHORT_LIVED_ACCESS_TOKEN_SIGNING - TLS termination in place; WebSocket upgrades pass through the proxy
- S3 bucket configured if your app stores blobs
- Quotas set:
DEFAULT_STORAGE_SIZE_QUOTA,QUOTA_COUNT_KV_RECORDS,QUOTA_COUNT_LT_RECORDS - Redis configured if running more than one instance
Never expose a server started from the development docker-compose.yml to
the internet - it runs with AUTH_DEV_MODE=true, which lets anyone log in as
the built-in test accounts without a password.