# 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= # 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./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