Self-hosting
Gate is one Next.js application and one PostgreSQL database. The portal, HTTP API, OIDC provider, Stripe integration, and background worker all run in the same process. No cloud service is required.
Requirements
- An already configured PostgreSQL 16+ database and connection credentials
- Docker Engine on Linux AMD64 for published images
- Node.js 26.5.0 and pnpm 12.3.4 when running from source
Environment variables
| Variable | Default | Notes |
|---|---|---|
DATABASE_URL | postgres://gate:gate@127.0.0.1:5432/gate | The only infrastructure dependency |
BETTER_AUTH_SECRET | development placeholder | Minimum 32 characters. Also the HMAC key for service API keys |
GATE_BASE_URL | resolved from NODE_ENV | Must be the public origin. Determines the issuer |
GATE_CONFIG_MASTER_KEY | 32 zero-ish bytes, development only | Base64, exactly 32 bytes decoded |
GATE_CONFIG_KEY_VERSION | 1 | Positive integer. Bind to a re-encryption migration |
GATE_ALLOWED_AUDIENCES | empty | Comma-separated extra OAuth audiences |
GATE_BASE_URL defaults to https://evonia-gate.annatarhe.com when NODE_ENV is production and
to http://localhost:3621 otherwise, so a standard deployment never sets it. Self-hosting on another
domain does, and the value must match the public origin exactly.
In production, assertProductionEnvironment() refuses to start if DATABASE_URL,
BETTER_AUTH_SECRET, or GATE_CONFIG_MASTER_KEY is unset, or if the master key does not decode to
exactly 32 bytes.
Generate the two secrets independently:
openssl rand -base64 48 # BETTER_AUTH_SECRET
openssl rand -base64 32 # GATE_CONFIG_MASTER_KEYWhat is not configurable
Trusted origins, CORS origins, and Stripe return-URL origins all come from one hard-coded list in
src/server/origins.ts. Adding a product origin means editing that file — one place, but still a
code change rather than configuration.
Audiences are the exception: GATE_ALLOWED_AUDIENCES extends them at runtime.
Local setup
cp .env.example .env.local
pnpm install --frozen-lockfile
# Set DATABASE_URL to your existing database and fill in the secrets first.
node --env-file=.env.local node_modules/drizzle-kit/bin.cjs migrate
pnpm devGate listens on port 3621. Create an account, have your database administrator grant Gate administrator access, then create an organization and project. Registration alone does not grant dashboard access.
When the schema changes, run pnpm db:generate and commit the SQL and metadata under drizzle/.
Docker
Prepare .env.production with your existing DATABASE_URL, generated BETTER_AUTH_SECRET and
GATE_CONFIG_MASTER_KEY, and your public GATE_BASE_URL. Use plain KEY=value lines, without
shell quotes or interpolation. Gate does not provision PostgreSQL.
set -e
# Select a published release tag or digest.
GATE_IMAGE=ghcr.io/evoniaai/gate:v0.1.1
docker pull "$GATE_IMAGE"
docker run --rm --network host --env-file .env.production \
"$GATE_IMAGE" node scripts/migrate.mjs
docker run -d --name gate --restart unless-stopped --network host \
--env-file .env.production -e HOSTNAME=127.0.0.1 "$GATE_IMAGE"These commands use Linux host networking to reach your existing database and bind Gate to localhost
for a host TLS proxy. The image uses Next's standalone output on node:26.5.0-alpine and runs as a
non-root user. It applies committed migrations before starting; failed migration prevents startup.
Stop existing Gate replicas and back up the database before upgrades. Run only one migration
process at a time. After a successful migration, replace the stopped application container with the
new image. Application rollback does not undo database migrations. See the repository's
docs/deployment.md for the complete deployment, backup, and recovery procedure.
Reverse proxy
Put an ordinary TLS terminator in front of port 3621 and forward Host, X-Forwarded-Proto, and
X-Forwarded-For. GATE_BASE_URL must match the public origin exactly, because it determines the
issuer that every relying party verifies.
Health check: GET /api/v1/health, which runs select 1.
Background jobs
The job runner starts from src/instrumentation.ts and polls every 5 seconds. Every /api/v1
request also drains pending jobs after responding.
Jobs are claimed with FOR UPDATE SKIP LOCKED, retried up to 10 times, and recovered if they sit in
processing for more than five minutes. Running one job runner per application replica is safe and
expected — concurrent replicas will not double-process.
Endpoints to expose
| Purpose | Path |
|---|---|
| Portal | /dashboard |
| Documentation | /docs |
| API | /api/v1 |
| OpenAPI | /api/v1/openapi.json |
| OpenAPI explorer | /docs/api-reference |
| OIDC discovery | /.well-known/openid-configuration |
| OAuth metadata | /.well-known/oauth-authorization-server |
| JSON Web Keys | /.well-known/jwks.json |
| Auth and OIDC routes | /api/auth/* |
| Stripe webhook | /api/v1/webhooks/stripe/{environmentId} |
| Health | /api/v1/health |
The .well-known routes must be reachable at the root, not under a path prefix. Relying parties
find everything else through discovery, but they find discovery by convention.
Backups
Back up PostgreSQL and GATE_CONFIG_MASTER_KEY separately. A database backup without the master
key cannot decrypt secrets or Stripe credentials; the master key alone is useless. Storing them
together defeats the encryption.
Rolling out to a product
For each product, create one project, register its browser, CLI, and mobile clients, and issue an environment-scoped service key to its backend.
During migration, accept both the product's existing token and the Gate token for a period, then move sign-in over. Rawback keeps Apple in-app purchase as a product-specific payment source and writes the resulting entitlement grants to Gate; new Stripe billing goes through Gate directly.
Verification
pnpm lint
pnpm fmt:check
pnpm typecheck
pnpm test
pnpm build