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

VariableDefaultNotes
DATABASE_URLpostgres://gate:gate@127.0.0.1:5432/gateThe only infrastructure dependency
BETTER_AUTH_SECRETdevelopment placeholderMinimum 32 characters. Also the HMAC key for service API keys
GATE_BASE_URLresolved from NODE_ENVMust be the public origin. Determines the issuer
GATE_CONFIG_MASTER_KEY32 zero-ish bytes, development onlyBase64, exactly 32 bytes decoded
GATE_CONFIG_KEY_VERSION1Positive integer. Bind to a re-encryption migration
GATE_ALLOWED_AUDIENCESemptyComma-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:

curl
openssl rand -base64 48   # BETTER_AUTH_SECRET
openssl rand -base64 32   # GATE_CONFIG_MASTER_KEY

What 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

curl
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 dev

Gate 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.

curl
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

PurposePath
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

curl
pnpm lint
pnpm fmt:check
pnpm typecheck
pnpm test
pnpm build