Skip to main content
This guide covers the current runtime configuration path for running Overlay outside the default Vercel, WorkOS, Stripe, and Cloudflare R2 SaaS setup. For enterprise IT teams that want source visibility and customization, use Enterprise Source Install:
That path runs the official image by default while keeping the full source checkout available for audit, configuration, and source-level customization. For the simplest pilot/operator path without a source checkout, use the one-command installer:
That installer creates /opt/overlay/overlay.config.json, /opt/overlay/.env, /opt/overlay/docker-compose.yml, and an overlayctl manager. See One-Command Install for install options, commands, and common config edits. For tenant boundaries and enterprise role modeling, see Tenancy And Role Model. The short version: self-hosted and managed-cloud enterprise deployments are single-customer deployments; students, teachers, parents, admins, departments, and classes are roles or groups inside that deployment, not tenants. For licensing and brand constraints, see Licensing and Legal Self-Hosting Notes. The short version: core product code is AGPL-3.0-or-later, reusable SDK/contract packages are Apache-2.0, and modified distributions need their own branding unless LayerNorm Inc. gives written permission. The app now loads configuration from three layers, highest precedence first:
  1. Environment variables.
  2. OVERLAY_CONFIG_FILE, normally overlay.config.json.
  3. The default config in src/shared/config/defaultOverlayRuntimeConfig.ts.
Use the examples in Deployment Profiles as starting points. They are validated by npm run docs:health.

Deployment Models

See Managed Cloud Enterprise for the managed-cloud option and Tenancy And Role Model for the tenant boundary.

Required Environment Variables

Minimum app runtime variables: Provider-specific variables: Enterprise runtime config v2 overrides: Optional observability/integration variables:

Configure overlay.config.json

Create overlay.config.json at the repo root or point OVERLAY_CONFIG_FILE to another path:
The normalized shape is: Enterprise config v2 adds these top-level sections while keeping all keys above backward compatible: Environment variables override JSON values. Use that for secrets in deployed environments. For example, keep storage.s3.bucketName in JSON but inject S3_SECRET_ACCESS_KEY from your runtime secret manager. Validate a config before booting the app:
Before an on-prem release, run the complete enterprise release gate:
The manual UI checklist is in Release Gates.

Deployment Profiles

Validated examples live under docs/config: The examples contain placeholders only. Replace every placeholder or replace_with value before a real deployment.

Provider Swaps

Auth providers: Billing providers: Storage providers: LLM providers: Enterprise provider matrix:

Postgres app-data setup

For MCP servers, generate a separate credential-encryption key:
Store it as MCP_CREDENTIAL_ENCRYPTION_KEY on the web and worker runtimes. During rotation, place the old value in MCP_CREDENTIAL_ENCRYPTION_KEY_PREVIOUS, deploy the new primary key, rewrite saved credentials through the management API, then remove the previous key. MCP responses expose only hasAuth; bearer tokens and custom header values are AES-256-GCM encrypted at rest and are never returned by list APIs. Production MCP URLs must use HTTPS and pass Overlay’s SSRF validation. Each server has a default execution policy of allow, approval_required, or deny, plus optional per-tool overrides. Use approval_required as the enterprise default when a server contains write-capable tools. Tool name, argument hash, policy decision, result status, duration, and request context are persisted for audit without storing raw tool arguments. Postgres app-data support is split into phases. Operators can provision, validate, migrate, and smoke-test the app-data schema now. Run with billing.provider="none" and keep integrations and API keys disabled until their parity phases land. Projects, files, generated media, browser outputs, Daytona artifacts, saved memories, knowledge retrieval, scheduled automations, and signed outbound webhooks are supported. Postgres-native memory and knowledge search uses pgvector; do not keep Convex enabled in the browser as a hidden vector sidecar. Confirm vector is available before migration, then set:
Run the durable worker continuously. It executes memory extraction, indexing, failed-index recovery, retention, and embedding-model backfills:
When Postgres app-data uses automations or outbound webhooks, declare the supervised runtime explicitly in the web, worker, and scheduler environments:
The Postgres capability response hides automations and webhooks until the background runtime is declared. Automations additionally require INTERNAL_SERVICE_AUTH_SECRET. Set the declaration only when at least one scheduler and one worker are continuously supervised. When changing OVERLAY_EMBEDDING_MODEL_VERSION, deploy the new worker and run:
Repeat until queued is zero, then allow workers to drain the queue before retiring the old embedding model configuration. For local setup:
For cloud Postgres, set OVERLAY_DATABASE_URL and run:
Do not reuse BETTER_AUTH_DATABASE_URL for app data. Better Auth and Overlay app records have separate schemas and migration lifecycles.

Billing, API keys, and administrators

Postgres app-data supports Stripe subscription records, exactly-once top-up grants, and durable Stripe event deduplication. Register the Postgres deployment webhook as:
Set STRIPE_WEBHOOK_SECRET to that endpoint’s signing secret. Convex deployments continue to use the Convex Stripe webhook endpoint; do not register both endpoints for one deployment. API key values are returned only on create or rotation. The database stores an HMAC hash using API_KEY_HASH_SECRET, plus scopes, expiry, use metadata, and revocation state. Rotate the hash secret only through a planned key invalidation window because existing API keys cannot be re-hashed without their plaintext. Bootstrap the first administrator from a trusted maintenance shell after the user has signed in once and exists in the app-data users table:
Then use /api/v1/admin/principals for role grants/revocations and /api/v1/admin/audit for audit review. /app/admin exposes usage filters, institutional budget adjustment, and audit search; /api/v1/admin/usage is its provider-neutral API. Available roles are admin, auditor, billing_admin, and support. Only admin can grant or revoke roles, auditors can read audit records, and an administrator cannot revoke their own active role. Audit metadata is size-bounded and secret-shaped keys are redacted before insert. Users manage scoped credentials under Account settings. Before an AWS pilot, complete the AWS deployment, security, and release gate procedures for each enabled billing profile. For future vector-search planning, confirm that the managed Postgres provider exposes the vector extension:
This readiness command is read-only. It does not enable vector search, install extensions, create vector columns, or change the app-data schema. Postgres app-data does not use Convex crons. Run npm run app-db:worker as a separately supervised service and set OVERLAY_BACKGROUND_RUNTIME_ENABLED=true on every release component. Its Postgres scheduler enqueues maintenance, coordination cleanup, model-catalog refresh, S3 reconciliation, output retention, knowledge maintenance, due automation scans, and Daytona reconciliation. Leased workers execute jobs with retry, lease recovery, and dead-letter state. Automation occurrences are idempotent, each attempt is audited, overlapping runs obey the configured skip or queue policy, and cancellation is checked before execution. Outbound webhook events are deduplicated per subscription/event, signed over timestamp.payload with HMAC-SHA256, and retain delivery-attempt and dead-letter records. Signing secrets are returned only when a subscription is created or rotated; dead-letter redrive creates a new immutable delivery record. Daytona reconciliation adopts only correctly labeled workspaces, rejects unowned provider records, marks missing workspaces, and advances active metering windows idempotently. The standalone npm run app-db:maintenance command remains available for manual recovery. compliance.retention.fileDays governs generated media/browser outputs and compliance.retention.sandboxArtifactDays governs Daytona artifacts. Postgres chat realtime uses the conversation_events table and /api/v1/conversations/events long polling. Transactional pg_notify wakes one listener connection per web process; notifications are only an optimization because reconnects read from the durable cursor. It does not require sticky load-balancer sessions. Keep the worker running so expired event rows and message deltas are pruned. Each app tab holds at most one HTTP long-poll request, so account for concurrent tabs when setting ECS task count and reverse-proxy timeouts. The route waits up to 15 seconds; configure the ingress idle timeout above that value. The optional chat-stream relay provides exact SSE frame replay. It is not required for Postgres text durability: the web runtime continues consuming the model stream after a browser disconnect, persists batched text deltas, and reconnecting clients reload the authoritative message snapshot. The migration shape is:
  1. Consolidate repository/context boundaries before adding new persistence.
  2. Make Convex optional in the browser for Postgres mode.
  3. Add capability gates and route classification for every app surface.
  4. Implement chat with an explicit usage policy.
  5. Move business invariants into domain services.
  6. Add real backend contract tests for Convex and Postgres.
  7. Add Postgres-native vector search later through pgvector, after provider readiness and adapter work.
Capabilities: UI hiding is not an authorization boundary. Server routes also check capabilities and return deterministic capability_disabled responses when disabled.

Build and Deploy Without Vercel

Overlay is a Next.js app plus Convex. You can run the web process anywhere that can run Node and reach your Convex and object storage endpoints. Basic Node deployment:
Set NEXT_PUBLIC_APP_URL and NEXT_PUBLIC_CONVEX_URL in the app runtime. If your host injects env vars at runtime, prefer env vars for secrets and keep non-secret provider shape in overlay.config.json. Without Stripe:
Without WorkOS:
With OIDC instead of WorkOS:

Docker Compose

Use this path when an enterprise operator wants a source checkout with a plain Compose install. The default Compose file pulls a prebuilt ghcr.io/layernorm/overlay-web image; local source builds are opt-in through docker-compose.build.yml. This is a minimal on-prem shape for the app container. It assumes the customer already has S3-compatible object storage and does not include Convex because Convex remains a hosted deployment in the current architecture. Quick start:
docker-compose.yml mounts overlay.config.json into the container. The compose file intentionally does not run object storage, auth, database, browser automation, or sandbox services; those are enterprise-owned providers selected through config.
After editing source code, build locally with the override file:
Existing docker-compose.onprem.yml and .env.onprem.example files are retained as compatibility aliases for older operator instructions.

Secrets Placement

App runtime only:
  • SESSION_SECRET
  • SESSION_TRANSFER_KEY
  • SESSION_COOKIE_ENCRYPTION_KEY
  • INTERNAL_SERVICE_AUTH_SECRET
  • API_KEY_HASH_SECRET
  • PROVIDER_KEYS_SECRET
  • HOOKS_TOKEN_SALT
  • WorkOS API key and OIDC client secret
  • Stripe secret key, Stripe webhook secret
  • R2/S3 access key secrets
  • LLM provider API keys
Convex:
  • INTERNAL_API_SECRET must be set in Convex and must match the app runtime value for that deployment.
  • If you intentionally run direct Convex auth/debug paths or Convex Stripe sync actions, also configure the corresponding WorkOS or Stripe variables in that Convex deployment. The BFF-first web path should not require session cookie secrets in Convex.
Public and safe to expose:
  • NEXT_PUBLIC_APP_URL
  • NEXT_PUBLIC_CONVEX_URL
  • DEV_NEXT_PUBLIC_CONVEX_URL
  • NEXT_PUBLIC_POSTHOG_TOKEN
  • NEXT_PUBLIC_POSTHOG_HOST
  • Public CSP origins in app.cspConnectSrc
Values that must differ between staging and production:
  • NEXT_PUBLIC_APP_URL
  • NEXT_PUBLIC_CONVEX_URL and CONVEX_DEPLOYMENT
  • INTERNAL_API_SECRET
  • INTERNAL_SERVICE_AUTH_SECRET
  • SESSION_SECRET
  • SESSION_TRANSFER_KEY
  • SESSION_COOKIE_ENCRYPTION_KEY
  • API_KEY_HASH_SECRET
  • WorkOS client/API credentials
  • Stripe mode, keys, webhook secret, price IDs, and portal config
  • R2/S3 buckets and access credentials
  • LLM provider keys if you want separate billing/audit trails
  • PostHog, Sentry, and analytics project identifiers
Do not copy production secrets into staging to “make it work”. The config schema rejects several mixed staging/production combinations, but secret separation is still an operator responsibility.

Convex Notes

Push Convex schema/function changes to the matching deployment:
For local web work against the dev Convex backend, prefer:
Do not pass .env.local to production convex deploy; that file usually points at the dev slug. Use the package scripts so prod and dev deploys stay separate.

Verification

Programmatic checks:
Minimal on-prem smoke:
Verify:
  • App shell loads at http://localhost:3000/app/chat.
  • Chat page renders. With llm.gatewayProvider="none", message generation is intentionally unavailable until you configure an LLM provider.
  • Settings loads at http://localhost:3000/app/settings. In the minimal no-auth profile it should show the normal sign-in gate with SSO buttons hidden.
  • Redacted system/capability state is available at http://localhost:3000/api/v1/capabilities in non-production environments. /api/v1/bootstrap also includes redacted system state after normal app authentication.
  • Billing, SSO, automations, webhooks, API key management, and vector search routes return clear disabled responses when their capabilities are false.
  • File upload requires a signed-in user and the configured object store. With the minimal no-auth profile, upload routes should return Unauthorized; with S3 enabled, create the bucket before testing authenticated uploads.
If screenshots are added to this document later, verify they do not show secrets, API keys, private hostnames, or customer data.