/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:
- Environment variables.
OVERLAY_CONFIG_FILE, normallyoverlay.config.json.- The default config in
src/shared/config/defaultOverlayRuntimeConfig.ts.
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
Createoverlay.config.json at the repo root or point OVERLAY_CONFIG_FILE to another path:
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:
Deployment Profiles
Validated examples live underdocs/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: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:
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:
queued is zero, then allow workers to drain the queue before
retiring the old embedding model configuration.
For local setup:
OVERLAY_DATABASE_URL and run:
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: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:
/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:
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:
- Consolidate repository/context boundaries before adding new persistence.
- Make Convex optional in the browser for Postgres mode.
- Add capability gates and route classification for every app surface.
- Implement chat with an explicit usage policy.
- Move business invariants into domain services.
- Add real backend contract tests for Convex and Postgres.
- Add Postgres-native vector search later through
pgvector, after provider readiness and adapter work.
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: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:
Docker Compose
Use this path when an enterprise operator wants a source checkout with a plain Compose install. The default Compose file pulls a prebuiltghcr.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.
docker-compose.onprem.yml and .env.onprem.example files are retained as compatibility aliases for older operator instructions.
Secrets Placement
App runtime only:SESSION_SECRETSESSION_TRANSFER_KEYSESSION_COOKIE_ENCRYPTION_KEYINTERNAL_SERVICE_AUTH_SECRETAPI_KEY_HASH_SECRETPROVIDER_KEYS_SECRETHOOKS_TOKEN_SALT- WorkOS API key and OIDC client secret
- Stripe secret key, Stripe webhook secret
- R2/S3 access key secrets
- LLM provider API keys
INTERNAL_API_SECRETmust 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.
NEXT_PUBLIC_APP_URLNEXT_PUBLIC_CONVEX_URLDEV_NEXT_PUBLIC_CONVEX_URLNEXT_PUBLIC_POSTHOG_TOKENNEXT_PUBLIC_POSTHOG_HOST- Public CSP origins in
app.cspConnectSrc
NEXT_PUBLIC_APP_URLNEXT_PUBLIC_CONVEX_URLandCONVEX_DEPLOYMENTINTERNAL_API_SECRETINTERNAL_SERVICE_AUTH_SECRETSESSION_SECRETSESSION_TRANSFER_KEYSESSION_COOKIE_ENCRYPTION_KEYAPI_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
Convex Notes
Push Convex schema/function changes to the matching deployment:.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:- 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/capabilitiesin non-production environments./api/v1/bootstrapalso 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.