496 lines
24 KiB
Bash
496 lines
24 KiB
Bash
# Branding (optional overrides; defaultBrandTokens is used when these are unset)
|
|
# BRAND_PRODUCT_NAME=Acme Ops
|
|
# BRAND_SITE_URL=https://acme.example
|
|
# BRAND_SUPPORT_EMAIL=support@acme.example
|
|
# BRAND_SUPPORT_FROM_NAME=Acme Support
|
|
# BRAND_LEGAL_IMPRINT_URL=https://acme.example/imprint
|
|
# BRAND_LEGAL_PRIVACY_URL=https://acme.example/privacy
|
|
# BRAND_LEGAL_TERMS_URL=https://acme.example/terms
|
|
# BRAND_LOGO_URL=/android-chrome-512x512.png
|
|
# BRAND_FAVICON_URL=/favicon.ico
|
|
# BRAND_MAIL_PRIMARY=#2563eb
|
|
# BRAND_MAIL_PRIMARY_TEXT=#ffffff
|
|
# BRAND_MAIL_BACKGROUND=#f6f8fb
|
|
# BRAND_MAIL_LINK=#2563eb
|
|
|
|
# Core
|
|
APP_NAME=ToolCTRL
|
|
# DATABASE_URL: pooled connection for application traffic. Point at PgBouncer
|
|
# on port 6432 in environments where the pooler is deployed and append
|
|
# `?pgbouncer=true&connection_limit=10` so Prisma disables prepared-statement
|
|
# caching (transaction-mode pooling cannot keep them). DIRECT_URL (below) is
|
|
# the unpooled connection used by `prisma migrate`. See docs/ops/pgbouncer.md.
|
|
DATABASE_URL="postgres://user:pass@localhost:6432/boilerplate?pgbouncer=true&connection_limit=10"
|
|
# SESSION_SECRET signs the session cookie. Min 32 bytes of entropy recommended (openssl rand -hex 32).
|
|
SESSION_SECRET=
|
|
# CRYPTO_SECRET is the AES key for stored workflow credentials. Use a SEPARATE secret from SESSION_SECRET.
|
|
CRYPTO_SECRET=
|
|
APP_BASE_DOMAIN=localhost
|
|
WEB_APP_HOST=app.localhost
|
|
# Public app origin used for embed snippets, mail links, and worker-to-web callbacks.
|
|
# APP_URL=http://app.localhost:3000
|
|
ADMIN_APP_HOST=admin.localhost
|
|
API_HOST=api.localhost
|
|
# APP_PROTOCOL=http
|
|
|
|
# Legacy API/auth tokens
|
|
API_ACCESS_TOKEN=
|
|
JWT_SECRET=
|
|
# Temporary cutover switch for plaintext API-key lookup. Secure-by-default: ships
|
|
# disabled. Set to `true` ONLY during the hash-metadata backfill window, then set
|
|
# back to `false` once the backfill is verified.
|
|
API_KEY_LEGACY_PLAINTEXT_FALLBACK=false
|
|
|
|
# Local development
|
|
# Set to true only for disposable local databases when you want sample tenants/users.
|
|
# SEED_DEMO_DATA=true
|
|
# DEMO_USER is read only when NODE_ENV=development.
|
|
# DEMO_USER=admin@email.com:password
|
|
# Production seed bootstrap credentials. Required for `NODE_ENV=production pnpm seed`.
|
|
# SEED_ADMIN_EMAIL=admin@example.com
|
|
# SEED_ADMIN_PASSWORD=replace-with-production-bootstrap-password
|
|
# WEBHOOKS_URL=https://...ngrok-free.app
|
|
|
|
# S3-compatible storage. HiveGrid prod/staging nutzt Hetzner Object Storage (ADR-009),
|
|
# Endpoint-Format z.B. https://nbg1.your-objectstorage.com (S3-API identisch; ebenso nutzbar: Wasabi, Backblaze B2, MinIO, AWS S3).
|
|
# Werte gehoeren in die Coolify-Secrets je Umgebung, nicht ins Repo. Leave STORAGE_ENDPOINT empty to use local filesystem (STORAGE_LOCAL_ROOT).
|
|
STORAGE_ENDPOINT=
|
|
STORAGE_REGION=
|
|
STORAGE_BUCKET=
|
|
STORAGE_ACCESS_KEY_ID=
|
|
STORAGE_SECRET_ACCESS_KEY=
|
|
STORAGE_SESSION_TOKEN=
|
|
STORAGE_PUBLIC_BASE_URL=
|
|
# Set to true for providers that require path-style bucket addressing (MinIO, some Wasabi regions).
|
|
# Leave false for AWS S3 default virtual-hosted style.
|
|
STORAGE_FORCE_PATH_STYLE=false
|
|
|
|
# Billing
|
|
STRIPE_SK=
|
|
STRIPE_WEBHOOK_SIGNING_SECRET=
|
|
STRIPE_ACCOUNT_COUNTRY=US
|
|
|
|
# Worker delivery credentials for queued transactional email (toolctrl.email -> Mailjet)
|
|
MAILJET_API_KEY=
|
|
MAILJET_SECRET_KEY=
|
|
MAILJET_FROM_EMAIL=
|
|
# MAILJET_FROM_NAME=
|
|
# Inbound Mailjet event webhook Basic-Auth password (api runtime). Callback URL: https://mailjet:SECRET@api.example.com/v1/email-sequences/mailjet/webhooks
|
|
# MAILJET_WEBHOOK_SECRET=
|
|
SUPPORT_EMAIL=
|
|
COMPANY_NAME=
|
|
COMPANY_ADDRESS=
|
|
|
|
# Contact and newsletter
|
|
INTEGRATIONS_CONTACT_FORMSPREE=
|
|
|
|
# Analytics (First-Party-Ingestion-Token; Google Analytics wurde EU-sterilisiert entfernt — R-P1a)
|
|
ANALYTICS_TOKEN=
|
|
|
|
# mCaptcha (self-hosted PoW captcha — see infra/mcaptcha/README.md). Public bits
|
|
# (instance URL + per-surface sitekeys) are safe to expose; the account secret is
|
|
# server-only. Verification fails open by default (CAPTCHA_FAIL_OPEN).
|
|
MCAPTCHA_INSTANCE_URL=
|
|
MCAPTCHA_ACCOUNT_SECRET=
|
|
MCAPTCHA_SITEKEY_LOGIN=
|
|
MCAPTCHA_SITEKEY_FORMS=
|
|
MCAPTCHA_SITEKEY_BOOKING=
|
|
CAPTCHA_FAIL_OPEN=true
|
|
AUTH_MAGIC_LINK_ENABLED=false
|
|
AUTH_MAGIC_LINK_TOKEN_TTL_MINUTES=15
|
|
|
|
# Optional notification-provider diagnostics
|
|
NOTIFICATIONS_NOVU_API_KEY=
|
|
|
|
# Queue and browser-backed workers
|
|
WORKER_ENABLED=false
|
|
# Worker queue group — one of general | mail | import | webhook | plugin. Required when
|
|
# WORKER_ENABLED=true. Each docker-compose / K8s replica sets exactly one group
|
|
# and that determines which BullMQ queues the process subscribes to. See
|
|
# docs/ops/worker-queues.md.
|
|
# WORKER_QUEUE_GROUP=general
|
|
# General worker workflow-trigger executor. Mode `http` uses the signed web
|
|
# bridge and falls back to APP_URL when URL is unset. Mode `hybrid` canaries the
|
|
# worker-native runner with HTTP fallback. Mode `native` is opt-in for supported
|
|
# safe-core workflows: manual/event triggers, if/switch, httpRequest, log,
|
|
# alertUser, doNothing, rowGet, iterator, variable, email, and single-boundary
|
|
# waitForInput continuation resumes.
|
|
# WORKFLOW_TRIGGER_EXECUTOR_MODE=http
|
|
# WORKFLOW_TRIGGER_EXECUTOR_URL=
|
|
# API_ACCESS_TOKEN must match the web runtime token when http/hybrid mode is used.
|
|
# WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS=30000
|
|
# WORKFLOW_TRIGGER_OUTBOX_ENABLED=true
|
|
# WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY=5
|
|
# WORKFLOW_TRIGGER_OUTBOX_REQUIRED=false
|
|
# Per-group concurrency (only the matching group's value is read at runtime).
|
|
# Defaults: general=10, mail=5, import=1, webhook=20, plugin=5.
|
|
# WORKER_CONCURRENCY_GENERAL=10
|
|
# WORKER_CONCURRENCY_MAIL=5
|
|
# WORKER_CONCURRENCY_IMPORT=1
|
|
# WORKER_CONCURRENCY_WEBHOOK=20
|
|
# WORKER_CONCURRENCY_PLUGIN=5
|
|
# Health HTTP port for /healthz and /readyz (per-worker). Defaults to 9100;
|
|
# the five-service worker layout uses 9101-9105.
|
|
# WORKER_HEALTH_PORT=9100
|
|
# WORKER_HEALTH_HOST=0.0.0.0
|
|
# Redis connection pool size advisory (forwarded to ioredis). Defaults differ
|
|
# per group: general/mail=4, import=2, webhook=8, plugin=4.
|
|
# WORKER_REDIS_POOL_SIZE=
|
|
EXPORT_RETENTION_QUEUE_ENABLED=false
|
|
IMPORT_QUEUE_ENABLED=false
|
|
# Outbox dispatcher (transactional outbox pattern; see docs/architecture/outbox-pattern.md).
|
|
# Defaults: off in dev so workers without Redis can still boot. Set true on staging+prod.
|
|
OUTBOX_DISPATCHER_ENABLED=false
|
|
OUTBOX_DISPATCHER_REQUIRED=false
|
|
OUTBOX_DISPATCH_BATCH_SIZE=100
|
|
OUTBOX_DISPATCH_CONCURRENCY=10
|
|
OUTBOX_DISPATCH_INTERVAL_MS=1000
|
|
OUTBOX_MAX_RETRIES=6
|
|
# Daily worker-general maintenance that purges DONE outbox rows after the replay/debug window.
|
|
OUTBOX_DONE_RETENTION_HOURS=720
|
|
OUTBOX_DONE_RETENTION_BATCH_SIZE=1000
|
|
BULLMQ_REDIS_URL=redis://localhost:6379/0
|
|
# REDIS_URL is a fallback only when BULLMQ_REDIS_URL is absent; keep it commented if a platform injects it.
|
|
# REDIS_URL=redis://localhost:6379/0
|
|
# Two-tier cache pub/sub invalidation (subscription + tenant; entitlements piggy-back on subscription).
|
|
# Set CACHE_INVALIDATION_ENABLED=false to disable cross-replica pub/sub (emergency switch — L1 TTL becomes
|
|
# the only consistency mechanism). See docs/architecture/two-tier-cache.md.
|
|
CACHE_INVALIDATION_ENABLED=true
|
|
CACHE_L1_TTL_SECONDS=300
|
|
# API idempotency-key storage backend. Default "prisma" keeps reservations in Postgres (multi-replica-safe).
|
|
# Set "shared-store" to back them with Redis instead (keeps the primary DB cooler; Redis TTL handles eviction).
|
|
IDEMPOTENCY_BACKEND=prisma
|
|
# Headless browser runtime (Playwright/Puppeteer-as-a-service) used by worker-backed browser sessions.
|
|
# Leave empty for local dev; set in production when workflow steps need real browser automation.
|
|
BROWSER_RUNTIME_URL=
|
|
BROWSER_RUNTIME_API_KEY=
|
|
BROWSER_SESSION_TIMEOUT_MS=60000
|
|
|
|
# IP enrichment providers used by audit/log enrichment. Pick ONE; if both are set IPStack is preferred.
|
|
# IPStack (ipstack.com) — paid tiers required for HTTPS and bulk lookups.
|
|
IPSTACK_API_KEY=
|
|
# ipapi.is — alternative provider with similar response shape; free tier available.
|
|
IPAPIIS_API_KEY=
|
|
PORTAL_SERVER_URL=
|
|
PORTAL_SERVER_FLY_TOKEN=
|
|
|
|
# Meilisearch — full-text search engine consumed by packages/search and the
|
|
# search-indexer worker job. MEILI_API_KEY is the SaaS-scoped client key the app
|
|
# authenticates with; it is a server-side secret — never expose it to browsers.
|
|
# Local dev uses the docker-compose meilisearch service whose master key defaults
|
|
# to `devkey`, so the app key is `devkey` too. Production injects a scoped key
|
|
# (infra STANDARDS §4 / docs/deploy/meilisearch.md, docs/features/search.md).
|
|
MEILI_URL=http://localhost:7700
|
|
MEILI_API_KEY=devkey
|
|
# MEILI_ENV=development
|
|
# Toggle search backend hot-path strategy. `meilisearch` (default) goes
|
|
# through the wrapper with automatic SQL-LIKE fallback on connection
|
|
# errors; `sql` forces every read onto the legacy fallback (emergency
|
|
# switch). See docs/features/search.md#rollback.
|
|
# SEARCH_BACKEND=meilisearch
|
|
|
|
# Dynamic site API keys
|
|
# SITE_KEY_EXAMPLE_SITE=
|
|
|
|
# Production/runtime platform
|
|
# NODE_ENV=production
|
|
# DIRECT_URL: unpooled Postgres URL (port 5432, bypasses PgBouncer). Required
|
|
# by `prisma migrate` / `db push` so prepared statements survive. Apps still
|
|
# use DATABASE_URL (above) for runtime queries. See docs/ops/pgbouncer.md.
|
|
# DIRECT_URL="postgres://user:pass@localhost:5432/boilerplate"
|
|
# DATABASE_READ_URL: optional Postgres read-replica used by getReadDbClient()
|
|
# for hot-read paths (CRM/KB listings, dashboards, public sites/portals).
|
|
# When unset or empty, getReadDbClient() falls back to DATABASE_URL so single-
|
|
# DB dev and self-hosted setups work without any code change. Point at the
|
|
# replica (or a PgBouncer-fronted replica) in staging/production. Replication
|
|
# is eventually consistent — see docs/architecture/rw-split.md.
|
|
# DATABASE_READ_URL="postgres://read_user:pass@replica.localhost:5432/boilerplate"
|
|
# SERVER_URL=https://app.example.com
|
|
# VERCEL_URL=
|
|
|
|
# Observability (opt-in; leave commented to keep the safe noop reporter and disabled tracing)
|
|
# SENTRY_DSN=https://public@sentry.example/1
|
|
# API (apps/api) and Worker (apps/worker) — independent DSNs so the two
|
|
# processes report to separate Sentry projects (boilerplate-api / boilerplate-worker).
|
|
# SENTRY_DSN_API=https://public-api@sentry.example/3
|
|
# SENTRY_DSN_WORKER=https://public-worker@sentry.example/4
|
|
# SENTRY_ENVIRONMENT=production
|
|
# SENTRY_RELEASE=
|
|
# SENTRY_TRACES_SAMPLE_RATE=0.1
|
|
# SENTRY_PROFILES_SAMPLE_RATE=0.0
|
|
# OpenTelemetry (opt-in; leave commented to keep the api/worker OTel bootstrap as a no-op).
|
|
# Endpoint examples: sentry-relay (broken on Sentry 25.12.1 self-hosted upstream nginx, see B-005),
|
|
# OTel collector (http://otel-collector:4318/v1/traces), Tempo, Jaeger.
|
|
# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
|
# Comma-separated key=value headers forwarded with every OTLP export.
|
|
# OTEL_EXPORTER_OTLP_HEADERS=x-sentry-auth=Sentry sentry_key=<public-key>
|
|
# OTEL_SERVICE_NAME=boilerplate-api
|
|
# OTEL_TRACES_SAMPLER=parentbased_traceidratio
|
|
# OTEL_TRACES_SAMPLER_ARG=0.1
|
|
# OTEL_LOG_LEVEL=debug
|
|
# Browser-side OTel (Vite-bundled). VITE_OTEL_EXPORTER_OTLP_ENDPOINT must be
|
|
# CORS-reachable from the user agent; gateway/relay MUST be rate-limited.
|
|
# VITE_OTEL_EXPORTER_OTLP_ENDPOINT=https://otel.example.com/v1/traces
|
|
# VITE_OTEL_SERVICE_NAME=boilerplate-web
|
|
# Web (apps/web) — separate Sentry project from the API/Worker stack.
|
|
# SENTRY_DSN_WEB=https://public-web@sentry.example/2
|
|
# VITE_SENTRY_DSN_WEB=https://public-web@sentry.example/2
|
|
# VITE_SENTRY_RELEASE=
|
|
# VITE_SENTRY_TRACES_SAMPLE_RATE=0.1
|
|
# VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.1
|
|
# VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=1.0
|
|
# Sentry source-map upload + release tagging (CI-only; never commit the auth token).
|
|
# Required by .github/workflows/_sentry-release.yml and the @sentry/vite-plugin step.
|
|
# SENTRY_AUTH_TOKEN=
|
|
# SENTRY_ORG=
|
|
# SENTRY_PROJECT=boilerplate-web
|
|
# SENTRY_URL=https://sentry.boiler.saas-ctrl.com
|
|
|
|
# Asset CDN (opt-in; leave commented to keep origin-relative asset URLs).
|
|
# When set at build time, Vite emits all asset URLs prefixed with this origin
|
|
# so the CDN (Bunny.net by default — see docs/ops/asset-cdn.md) can pull from
|
|
# the app server and serve users from the edge. Local dev ignores the var.
|
|
# ASSET_CDN_URL=https://cdn.boiler.saas-ctrl.com
|
|
# Bunny.net pull-zone id used by the post-deploy purge step. CI-only.
|
|
# BUNNY_PULL_ZONE_ID=
|
|
# Bunny.net account-level API key for cache-purge calls. CI-only.
|
|
# BUNNY_API_KEY=
|
|
|
|
# Password policy (auth.password.errors.* messages; defaults match OWASP 2024).
|
|
# Override at deploy time to relax or tighten without a code deploy.
|
|
# AUTH_PASSWORD_MIN_LENGTH=12
|
|
# AUTH_PASSWORD_REQUIRE_UPPER=true
|
|
# AUTH_PASSWORD_REQUIRE_LOWER=true
|
|
# AUTH_PASSWORD_REQUIRE_DIGIT=true
|
|
# AUTH_PASSWORD_REQUIRE_SPECIAL=false
|
|
# AUTH_PASSWORD_HIBP_CHECK=true
|
|
# AUTH_PASSWORD_HIBP_MAX_OCCURRENCES=0
|
|
|
|
# Prometheus metrics + /status endpoints (api, worker, web).
|
|
# When METRICS_ENABLED=true both endpoints are mounted; /status is public,
|
|
# /metrics requires Authorization: Bearer $METRICS_TOKEN.
|
|
# METRICS_ENABLED=false
|
|
# METRICS_TOKEN=
|
|
# Worker admin HTTP server (only opened when METRICS_ENABLED=true).
|
|
# Keep it on a private network — Prometheus only.
|
|
# WORKER_ADMIN_PORT=9100
|
|
# WORKER_ADMIN_HOST=0.0.0.0
|
|
# Region tag surfaced in /status. Falls back to FLY_REGION when unset.
|
|
# APP_REGION=
|
|
|
|
# Backup CLI (@toolctrl/cli). See docs/operations/backup.md for the full runbook.
|
|
# BACKUP_S3_ENDPOINT=https://s3.example.com
|
|
# BACKUP_S3_REGION=us-east-1
|
|
# BACKUP_S3_BUCKET=toolctrl-backups
|
|
# BACKUP_S3_ACCESS_KEY_ID=
|
|
# BACKUP_S3_SECRET_ACCESS_KEY=
|
|
# BACKUP_S3_SESSION_TOKEN=
|
|
# BACKUP_S3_FORCE_PATH_STYLE=true
|
|
# Encryption: age (default), gpg, or none. age REQUIRES BACKUP_AGE_PUBLIC_KEY.
|
|
# BACKUP_ENCRYPTION=age
|
|
# Public-key recipient for age encryption (operator generates via `age-keygen`).
|
|
# BACKUP_AGE_PUBLIC_KEY=age1exampleexampleexampleexampleexampleexampleexampleexample
|
|
# Identity file used for `backup:restore` and `backup:verify` decryption.
|
|
# BACKUP_AGE_IDENTITY_FILE=/secrets/age-backup.key
|
|
# GPG fallback key id (only used when BACKUP_ENCRYPTION=gpg).
|
|
# BACKUP_GPG_RECIPIENT=
|
|
# Retention policy (most-recent kept per classification).
|
|
# BACKUP_RETENTION_DAILY=7
|
|
# BACKUP_RETENTION_WEEKLY=4
|
|
# BACKUP_RETENTION_MONTHLY=12
|
|
# Set to true to include a ClickHouse BACKUP statement in `backup:create`.
|
|
# BACKUP_INCLUDE_CLICKHOUSE=false
|
|
|
|
# Embeddable form widget (Phase 05 #58 — packages/form-widget).
|
|
# FORM_WIDGET_CDN_URL serves /embed/widget.js. Falls back to APP_URL when unset.
|
|
# Captcha on form submissions uses the shared mCaptcha instance (MCAPTCHA_* at
|
|
# the top of this file), opt-in per form AND gated behind the
|
|
# `form-widget-turnstile` feature flag. See docs/features/form-widget.md.
|
|
# FORM_WIDGET_CDN_URL=https://forms.example.com
|
|
|
|
# Marketing-funnel (Phase 05 #78 — apps/web/public/embed/marketing.js).
|
|
# MARKETING_ALLOWED_ORIGINS is a CSV of origins (with `*.host` wildcards
|
|
# allowed) from which the public `/api/leads/capture` endpoint accepts
|
|
# submissions. Fail-closed: empty list rejects all browser submissions.
|
|
# MARKETING_RATE_LIMIT_PER_MIN is the per-IP submission limit (default 5).
|
|
# Captcha is the shared mCaptcha instance (MCAPTCHA_*); flag is gated via
|
|
# the `marketing-funnel-turnstile` feature flag.
|
|
# See docs/features/marketing-funnel.md.
|
|
# MARKETING_ALLOWED_ORIGINS=https://marketing.example.com,https://*.example.com
|
|
# MARKETING_RATE_LIMIT_PER_MIN=5
|
|
|
|
# Marketing-funnel Phase 2 (Phase 05 #79 — self-service conversion +
|
|
# outbound webhooks + attribution reports).
|
|
# LEAD_CONVERSION_SECRET is the HMAC key (min 32 bytes) signing the
|
|
# `/register?leadId=…&token=…&plan=…` deep-links produced by
|
|
# `signLeadToken` and verified by the register loader. Rotate by setting a
|
|
# new value; old tokens become invalid after 7 days (default TTL).
|
|
# LEAD_CONVERSION_ENABLED=false short-circuits both the deep-link
|
|
# pre-fill flow and the admin "Convert to Tenant" button without rolling
|
|
# back the migration — useful for emergency disable.
|
|
# MARKETING_WEBHOOKS_ENABLED=false freezes the apps/worker
|
|
# `outbox.marketing` queue — outbox events accumulate so they replay
|
|
# automatically after the flip back to true (raise outbox retention if
|
|
# you plan a long pause).
|
|
# See docs/features/marketing-funnel.md.
|
|
# LEAD_CONVERSION_SECRET=
|
|
# LEAD_CONVERSION_ENABLED=true
|
|
# MARKETING_WEBHOOKS_ENABLED=true
|
|
|
|
# Marketing-Funnel Phase 3 — email-sequence engine (Phase 05 #80).
|
|
# MARKETING_TOKEN_SECRET signs the unsubscribe tokens embedded in every
|
|
# sequence email's `List-Unsubscribe` header. Falls back to
|
|
# LEAD_CONVERSION_SECRET when unset — both must be at least 32 bytes
|
|
# (`openssl rand -hex 32`).
|
|
# MARKETING_SEQUENCES_ENABLED must be set to true before apps/worker starts
|
|
# the `email-sequence.step` queue, trigger handler, or sequence cron producers.
|
|
# Keep this unset or false in production until the delayed producer, renderer,
|
|
# and mail-send adapter follow-ups are wired and verified.
|
|
# See docs/features/email-sequences.md.
|
|
# MARKETING_TOKEN_SECRET=
|
|
# MARKETING_SEQUENCES_ENABLED=false
|
|
|
|
# Realtime collaboration (Phase 05 #59 — apps/hocuspocus + Novel-Editor).
|
|
# HOCUSPOCUS_AUTH_SECRET signs the 15-min JWTs the web app issues via
|
|
# `/api/collab/token`. The Hocuspocus container verifies them in
|
|
# `onAuthenticate`; secret must match across web + hocuspocus deploys.
|
|
# Min 32 bytes of entropy (`openssl rand -hex 32`).
|
|
# VITE_HOCUSPOCUS_URL is the public WebSocket URL the browser connects to
|
|
# (e.g. `wss://collab.example.com`). When unset, the Novel-Editor mounts
|
|
# without collab extensions — graceful single-user fallback.
|
|
# HOCUSPOCUS_URL is the server-internal WebSocket endpoint exposed by the
|
|
# hocuspocus container (defaults to ws://hocuspocus:1234 in docker).
|
|
# HOCUSPOCUS_MAX_PAYLOAD_BYTES caps the Y.js state we persist per doc.
|
|
# Activation also requires the `realtime-collab-enabled` feature flag.
|
|
# See docs/features/realtime-collab.md.
|
|
# HOCUSPOCUS_AUTH_SECRET=
|
|
# VITE_HOCUSPOCUS_URL=wss://collab.example.com
|
|
# HOCUSPOCUS_URL=ws://hocuspocus:1234
|
|
# HOCUSPOCUS_PORT=1234
|
|
# HOCUSPOCUS_HOST=0.0.0.0
|
|
# HOCUSPOCUS_MAX_PAYLOAD_BYTES=10485760
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Admin-Panel SSO via Authentik (Phase 09 #82 — apps/web/app/modules/admin-sso)
|
|
# ---------------------------------------------------------------------------
|
|
# AUTHENTIK_ISSUER_URL is the OIDC issuer published by the Authentik
|
|
# `toolctrl-admin-panel` provider (matches the `iss` claim in ID-tokens).
|
|
# Typically `https://auth.<domain>/application/o/toolctrl-admin-panel/`.
|
|
# AUTHENTIK_ADMIN_CLIENT_ID must equal the blueprint client_id
|
|
# (`toolctrl-admin-panel` per infra/authentik/bootstrap/blueprints/03-*).
|
|
# AUTHENTIK_ADMIN_CLIENT_SECRET is fetched from the Authentik admin UI
|
|
# after the blueprint applies; rotate via Authentik then redeploy.
|
|
# AUTHENTIK_ADMIN_REDIRECT_URI is the exact callback URI registered with
|
|
# the Authentik provider — must match the deployed admin host.
|
|
# AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI is the URI Authentik redirects
|
|
# the browser to after end_session_endpoint; default falls back to the
|
|
# admin login page with `?logged_out=1`.
|
|
#
|
|
# ADMIN_FALLBACK_LOGIN_ENABLED gates the email/password emergency path.
|
|
# When `false` (default) the admin login page hides the password form
|
|
# entirely. ADMIN_FALLBACK_LOGIN_EMAILS is a comma-separated allowlist of
|
|
# admin emails that may use the fallback when the flag is on — keep it
|
|
# short (1-2 bootstrap accounts max). Each fallback login emits an
|
|
# `admin.login.fallback.success` audit event tagged severity=warn.
|
|
# See docs/operations/admin-sso.md.
|
|
# AUTHENTIK_ISSUER_URL=https://auth.example.com/application/o/toolctrl-admin-panel/
|
|
# AUTHENTIK_ADMIN_CLIENT_ID=toolctrl-admin-panel
|
|
# AUTHENTIK_ADMIN_CLIENT_SECRET=
|
|
# AUTHENTIK_ADMIN_REDIRECT_URI=https://admin.example.com/auth/admin/callback
|
|
# AUTHENTIK_ADMIN_POST_LOGOUT_REDIRECT_URI=https://admin.example.com/admin/login?logged_out=1
|
|
# ADMIN_FALLBACK_LOGIN_ENABLED=false
|
|
# ADMIN_FALLBACK_LOGIN_EMAILS=
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Capacitor mobile + push backend (Phase 08 #65 — apps/mobile + apps/worker)
|
|
# ---------------------------------------------------------------------------
|
|
# FCM_SERVICE_ACCOUNT_JSON holds the Firebase Cloud Messaging service account
|
|
# credentials used by the worker push job to dispatch Android notifications.
|
|
# Accepted formats: (1) raw JSON, (2) base64-encoded JSON. Generate via the
|
|
# Firebase console (Project settings → Service accounts → Generate new
|
|
# private key). NEVER commit this — it grants full FCM send rights.
|
|
#
|
|
# APNS_KEY is the PEM contents of the .p8 Apple Push Notification Service
|
|
# signing key (a standard PKCS#8 PEM block). APNS_KEY_ID is the 10-character
|
|
# key id shown in the Apple Developer console next to the generated .p8 file.
|
|
# APNS_TEAM_ID is the 10-character developer team id.
|
|
# APNS_BUNDLE_ID defaults to de.toolctrl.app (matches capacitor.config.ts
|
|
# > appId); override only when shipping a re-branded fork.
|
|
#
|
|
# MOBILE_LIVE_RELOAD_URL is a dev-only convenience: when set, apps/mobile
|
|
# emits a Capacitor `server.url` in capacitor.config.ts so the native shell
|
|
# loads the running web dev server (typically http://192.168.x.x:3000)
|
|
# instead of the bundled webDir. Leave empty for production builds.
|
|
# See docs/operations/capacitor.md.
|
|
# FCM_SERVICE_ACCOUNT_JSON=
|
|
# APNS_KEY=
|
|
# APNS_KEY_ID=
|
|
# APNS_TEAM_ID=
|
|
# APNS_BUNDLE_ID=de.toolctrl.app
|
|
# MOBILE_LIVE_RELOAD_URL=
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# AI-cost Phase 3 — Stripe overage billing (apps/worker cron + reconcile)
|
|
# ---------------------------------------------------------------------------
|
|
# AI_OVERAGE_SUBMISSION_ENABLED is the master killswitch for the nightly
|
|
# Stripe Billing Meter event submitter. When `false` (or `0`), AI events are still
|
|
# marked `isOverage = true` + `markedUpCostCents` populated, but the cron
|
|
# skips Stripe. Default: enabled when STRIPE_SK is configured. Use this to
|
|
# pause submissions during an incident; the next enabled run backfills via
|
|
# `stripeReportedAt IS NULL`.
|
|
#
|
|
# Per-plan markup multiplier + Stripe identifiers ship in
|
|
# `packages/entitlements/src/ai-overage.ts` (`standardAIOverageDefaults`).
|
|
# Configure live Stripe ids via /admin/settings/pricing/ai-overage (admin
|
|
# UI writes an audit log) and/or per-tenant overrides on
|
|
# `Tenant.aiQuotaOverrideJson` — see docs/features/ai-cost-billing.md.
|
|
# AI_OVERAGE_SUBMISSION_ENABLED=true
|
|
|
|
# Phase 06: Plugin output sanitization (strict|moderate|off)
|
|
PLUGIN_OUTPUT_SANITIZATION_STRICTNESS=moderate
|
|
|
|
# Phase 06: production runtime rollout gate. Leave false until the
|
|
# external plugin runtime security sign-off is complete. Production
|
|
# install/grant/enable and plugin workers fail closed unless true.
|
|
PLUGIN_RUNTIME_PRODUCTION_ENABLED=false
|
|
# Optional fail-closed switch for worker-general plugin hook fanout. In
|
|
# production with PLUGIN_RUNTIME_PRODUCTION_ENABLED=true and Redis present,
|
|
# worker-general already treats plugin hook fanout as required; set this only
|
|
# to force the same startup behavior in staging/canary environments.
|
|
# PLUGIN_HOOK_FANOUT_REQUIRED=false
|
|
|
|
# Phase 06: plugin worker sandbox limits. Trusted keys are a CSV of hex
|
|
# Ed25519 public keys; empty fails signature verification closed.
|
|
# PLUGIN_TRUSTED_PUBLIC_KEYS=
|
|
PLUGIN_MAX_FUEL=1000000
|
|
PLUGIN_MAX_MEMORY_MB=64
|
|
PLUGIN_MAX_WALL_CLOCK_MS=5000
|
|
PLUGIN_RATE_LIMIT_PER_MINUTE=100
|
|
PLUGIN_RATE_LIMIT_WINDOW_SECONDS=60
|
|
|
|
# Phase 06: PluginCapabilityUse audit retention (days)
|
|
PLUGIN_AUDIT_RETENTION_DAYS=90
|
|
PLUGIN_AUDIT_RETENTION_BATCH_SIZE=1000
|
|
PLUGIN_AUDIT_RETENTION_MAX_BATCHES=100
|
|
PLUGIN_AUDIT_RETENTION_CRON="0 3 * * *"
|
|
|
|
# Phase 06: optional override for host version used in plugin compat check (default: package.json version)
|
|
PLUGIN_HOST_VERSION_OVERRIDE=
|
|
|
|
# Phase 06 / F-005 Phase B: dev escape hatch to skip the on-disk binary
|
|
# signature verify at install time. MUST NOT be set in production.
|
|
PLUGIN_INSTALL_SKIP_BINARY_VERIFY=false
|
|
|
|
# Phase 06 / F-001: explicit per-tenant capability consent.
|
|
# When "true" (default in production), enablePlugin refuses to flip
|
|
# Plugin.enabled=true until every manifest-declared capability has a
|
|
# matching TenantPluginCapabilityGrant row. Set "false" for dev /
|
|
# integration-test environments that pre-date F-001.
|
|
PLUGIN_REQUIRE_CAPABILITY_CONSENT=true
|
|
|
|
# Phase 06 / F-011: per-tenant install rate limit (sliding window, 60s).
|
|
PLUGIN_INSTALL_RATE_LIMIT_PER_MIN=5
|
|
PLUGIN_SETTINGS_WRITE_RATE_LIMIT_PER_MIN=60
|
|
PLUGIN_SETTINGS_FRAME_VIOLATION_RATE_LIMIT_PER_MIN=120
|
|
PLUGIN_RATE_LIMIT_FAIL_OPEN=false
|