mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-14 21:59:30 +02:00
* feat(featureflag): framework-level feature flag system (MUL-3615) Introduces a reusable feature flag framework so future features can adopt flags without writing infrastructure code. Backend: server/pkg/featureflag (Go) - Service / Provider / Decision separation per Martin Fowler's Toggle Point / Toggle Router / Toggle Configuration pattern. - Providers: StaticProvider (rules in source control), EnvProvider (FF_<KEY> overrides for ops kill switches), ChainProvider (first-hit-wins composition). - EvalContext carried through context.Context with WithEvalContext / EvalContextFrom; supports user_id, workspace_id, free-form attributes. - PercentRollout via deterministic FNV-1a bucketing; same user always lands in the same bucket so experiments do not flap between requests. - Nil-safe Service: a nil *Service or missing flag returns the caller's default so business code never panics on a missing flag. - 100% unit-test coverage with -race; go vet clean. Frontend: packages/core/feature-flags (TypeScript) - Same vocabulary as the Go side (Decision, EvalContext, Rule, PercentRollout). FNV-1a parity ensures cross-tier bucket agreement. - FeatureFlagService + StaticProvider + ChainProvider in pure TS. - React glue: FeatureFlagsProvider, useFlag(key, default), useVariant(key, default). Hooks fall back to the default when no provider is mounted so Storybook / unit tests stay simple. - Vitest tests for service, providers, hash, and React hooks. Docs: docs/feature-flags.md — wiring, EvalContext, toggle points, backend-protection note, and the standard best-practice checklist. The framework intentionally has no third-party Go deps and no API surface beyond what real callers will need. New providers (DB, remote config, LaunchDarkly) plug in by implementing Provider; no existing caller has to change. Co-authored-by: multica-agent <github@multica.ai> * fix(featureflag): cross-tier hash parity + variant only when enabled (MUL-3615) Two must-fix issues from the PR review on #4496: 1. TS hash had a trailing zero separator that Go did not emit, so the same (key, identifier) bucketed differently on the two tiers. The "user lands in the same bucket on server and client" promise was broken. For example billing_new_invoice/user-42 was bucket 97 in Go and bucket 11 in TS. Fix: TS fnv1a now emits the zero separator BETWEEN parts only, never after the last one, matching Go's hash.Write byte stream exactly. Verified by parallel golden tests on both sides that pin five (key, identifier) -> bucket triples; if either side drifts both tests fail and one must be brought back in sync. 2. StaticProvider returned `Rule.Variant` regardless of whether the rule evaluated to enabled=true. A 0%-rollout user, a deny-listed user, or a default-off user would see variant="experiment-v2", so callers branching on Variant() would route control users into the experiment arm. Fix: Rule.Variant is now the ON-variant only. When the rule evaluates to enabled=false the Decision's variant is the canonical "off", regardless of what Rule.Variant says. Documented as a behavior contract in the Rule godoc / JSDoc and covered by regression tests on both sides. Tests: - go test -race ./pkg/featureflag/... : all green (1.58s). - pnpm --filter @multica/core test : 661/661 (3 new). - pnpm --filter @multica/core typecheck: clean. Co-authored-by: multica-agent <github@multica.ai> * fix(featureflag): hash UTF-8 bytes on the TS side for cross-tier parity (MUL-3615) Follow-up review on PR #4496 caught that the previous hash fix was only correct for ASCII input. The TS side used `charCodeAt`, which returns UTF-16 code units, while the Go side hashes the UTF-8 byte representation. Any non-ASCII flag key or identifier — Chinese flag names, accented user IDs, emoji — would bucket differently on backend vs frontend, silently breaking the "same user, same bucket" promise the PR description makes. Concretely: flag/é Go 53 vs TS-old 68 flag/🦄 Go 82 vs TS-old 75 实验/user-1 Go 90 vs TS-old 4 flag/用户-1 Go 95 vs TS-old 2 Fix: replace per-char charCodeAt with a module-level `TextEncoder` ('utf-8') and hash each encoded byte. After the fix all four cases above match Go exactly, and the existing ASCII cases continue to match. The cross-language golden tables on both sides now include the 5 new non-ASCII cases alongside the 5 ASCII cases, so any future regression that swaps UTF-8 for charCodeAt (or vice versa) will fail loudly on both Go and TS simultaneously. TextEncoder is part of WHATWG Encoding and is available in every evergreen browser, in Node 11+, and in Hermes (React Native) >= 0.74, which covers every runtime that imports @multica/core/feature-flags. Tests: - go test -race ./pkg/featureflag/... : all green. - pnpm --filter @multica/core test : 661/661. - pnpm --filter @multica/core typecheck : clean. Co-authored-by: multica-agent <github@multica.ai> * feat(featureflag): wire into main app config — YAML file + env override (MUL-3615) Follow-up requested by Yushen on PR #4496: make the feature flag framework configurable through the existing main-program config system instead of requiring Go code edits. multica's main app is purely env-var driven (see .env.example) with optional MULTICA_*_FILE knobs for richer config; feature flags now follow the same pattern. server/pkg/featureflag/config.go - LoadRulesFromYAMLFile(path) parses a YAML rule set into runtime Rule structs. Empty files are a valid "no flags yet" state; missing or malformed files surface a hard error so operators see misconfig the same way DATABASE_URL parse errors do. - NewServiceFromEnv composes the standard provider chain: 1. EnvProvider("FF_") (runtime kill-switch path) 2. StaticProvider from YAML file (declarative rule set) When MULTICA_FEATURE_FLAGS_FILE is unset, only the env layer is active and every IsEnabled call falls through to the caller's default, so the server can boot before any flag is authored. server/cmd/server/main.go - Construct the Service once at startup right after env-var warnings, fail loudly on malformed YAML, log the loaded rule count via the Service logger. The Service is held in a local `flags` variable ready to be threaded into handler.Handler / service constructors when the first flag user lands. Threading is deferred to the PR that adds the first business consumer so this PR stays a pure framework + config layer. .env.example - New "Feature flags" section documents MULTICA_FEATURE_FLAGS_FILE and the FF_<KEY> override convention, with a minimal YAML schema example inline. docs/feature-flags.md - Replace the "build a provider manually" example with the NewServiceFromEnv pattern that now matches what main.go actually does. Show the YAML schema in one place. Note the on-variant / off semantics from the previous review round. server/pkg/featureflag/doc.go - Update package doc to mention the gopkg.in/yaml.v3 dependency (already a server-level dep) instead of the now-inaccurate "no third-party dependencies" claim. Tests: - go test -race -count=1 ./pkg/featureflag/... all green; new config_test.go covers: simple YAML, full-shape YAML, empty file, missing file, malformed YAML, no env var, file-only, env-beats-file, bad file surfaces error. - go test -race -count=1 -run TestHealth ./cmd/server/... sanity check that the main.go boot path with the new wiring still passes. - go vet ./... clean. Co-authored-by: multica-agent <github@multica.ai> --------- Co-authored-by: Eve <eve@multica-ai.local> Co-authored-by: multica-agent <github@multica.ai>
301 lines
14 KiB
Plaintext
301 lines
14 KiB
Plaintext
# Database
|
|
POSTGRES_DB=multica
|
|
POSTGRES_USER=multica
|
|
POSTGRES_PASSWORD=multica
|
|
POSTGRES_PORT=5432
|
|
DATABASE_URL=postgres://multica:multica@localhost:5432/multica?sslmode=disable
|
|
# Optional pgxpool tuning. Defaults are 25 / 5 per pod and are usually fine.
|
|
# You can also set pool_max_conns / pool_min_conns as query params on
|
|
# DATABASE_URL; env vars below take precedence over URL params.
|
|
# DATABASE_MAX_CONNS=25
|
|
# DATABASE_MIN_CONNS=5
|
|
|
|
# Server
|
|
# APP_ENV gates production safety checks. Docker self-host pins APP_ENV to
|
|
# "production" by default. Local dev can leave it unset.
|
|
# See SELF_HOSTING.md for the full login setup.
|
|
APP_ENV=
|
|
# Optional local/testing shortcut. Empty by default, so there is no fixed
|
|
# verification code. Without RESEND_API_KEY, generated codes print to stdout.
|
|
# If you need deterministic local automation, set a 6-digit value such as
|
|
# 888888 and keep APP_ENV non-production. This is ignored when APP_ENV=production.
|
|
MULTICA_DEV_VERIFICATION_CODE=
|
|
PORT=8080
|
|
# Docker Compose consumes flat port values. Set BACKEND_PORT directly to
|
|
# override the backend host port.
|
|
BACKEND_PORT=8080
|
|
# Optional aliases for local/self-host backend port helpers outside compose.
|
|
# API_PORT=8080
|
|
# SERVER_PORT=8080
|
|
FRONTEND_PORT=3000
|
|
# Derived by docker-compose.selfhost.yml / local scripts from FRONTEND_PORT.
|
|
# Set explicitly only when serving frontend on a different origin/domain.
|
|
FRONTEND_ORIGIN=http://localhost:${FRONTEND_PORT}
|
|
# Prometheus metrics are disabled by default. When enabled, bind to loopback
|
|
# unless you protect the listener with private networking, allowlists, or
|
|
# proxy auth. Do not expose this endpoint through the public app/API ingress.
|
|
# HTTP request metrics start accumulating only when this listener is enabled.
|
|
# METRICS_ADDR=127.0.0.1:9090
|
|
JWT_SECRET=change-me-in-production
|
|
# Derived by Makefile / local scripts from the backend port.
|
|
# Set explicitly only when the daemon reaches the API through a different URL.
|
|
# MULTICA_SERVER_URL=ws://localhost:8080/ws
|
|
# Derived by docker-compose.selfhost.yml / local scripts from FRONTEND_ORIGIN.
|
|
# Set explicitly only when the app's public URL differs from local frontend.
|
|
MULTICA_APP_URL=${FRONTEND_ORIGIN}
|
|
# Public URL the API is reachable at from the open internet (no trailing
|
|
# slash). Used to mint absolute webhook URLs for autopilot webhook
|
|
# triggers and to show correct daemon setup commands in the web UI. Leave
|
|
# unset behind a same-origin reverse proxy or for plain localhost dev —
|
|
# the frontend will compose the URL from window.origin + webhook_path in
|
|
# that case. Headers are intentionally not used to derive this value, to
|
|
# avoid Host / X-Forwarded-Host spoofing when a self-hosted reverse proxy
|
|
# is not hardened.
|
|
MULTICA_PUBLIC_URL=
|
|
# Comma-separated CIDR list of reverse proxies whose X-Forwarded-For /
|
|
# X-Real-IP headers the per-IP webhook rate limiter is allowed to trust.
|
|
# Empty (the default) means "trust no headers" — the limiter uses
|
|
# r.RemoteAddr only, which is the safe shape when the backend is
|
|
# exposed directly. Set this when running behind nginx/Caddy/Cloudflare:
|
|
# e.g. "127.0.0.1/32" for a same-host reverse proxy, or the CDN's
|
|
# announced ranges for cloud deployments.
|
|
MULTICA_TRUSTED_PROXIES=
|
|
MULTICA_DAEMON_CONFIG=
|
|
MULTICA_WORKSPACE_ID=
|
|
MULTICA_DAEMON_ID=
|
|
MULTICA_DAEMON_DEVICE_NAME=
|
|
MULTICA_DAEMON_POLL_INTERVAL=3s
|
|
MULTICA_DAEMON_HEARTBEAT_INTERVAL=15s
|
|
MULTICA_CODEX_PATH=codex
|
|
MULTICA_CODEX_MODEL=
|
|
MULTICA_CODEX_WORKDIR=
|
|
MULTICA_CODEX_TIMEOUT=20m
|
|
|
|
# Feature flags
|
|
# Optional path to a YAML file declaring feature flag rules. When unset,
|
|
# every flag falls through to the caller's default, which lets the server
|
|
# boot before any flag config is authored. When set, the file is read once
|
|
# at startup and a parse / IO error fails fast — same loud-failure shape as
|
|
# DATABASE_URL or JWT_SECRET misconfig. See docs/feature-flags.md for the
|
|
# full schema; the minimum example is:
|
|
#
|
|
# billing_new_invoice_email:
|
|
# default: true
|
|
# checkout_algo:
|
|
# default: false
|
|
# variant: experiment-v2
|
|
# percent: { percent: 25, by: user_id }
|
|
#
|
|
# Individual flags can also be overridden without touching the YAML by
|
|
# setting FF_<FLAG_KEY> env vars (FF_BILLING_NEW_INVOICE_EMAIL=false, 25%,
|
|
# or any variant string). The env override beats the YAML, which is the
|
|
# Ops kill-switch path — flip a flag without redeploying by restarting the
|
|
# process with the env var set.
|
|
MULTICA_FEATURE_FLAGS_FILE=
|
|
|
|
# Self-host image channel
|
|
# Default stable release channel. Pin to an exact release like v0.2.4 if you
|
|
# want to stay on a specific version. If the selected tag has not been
|
|
# published to GHCR yet, use make selfhost-build / the build override instead.
|
|
MULTICA_IMAGE_TAG=latest
|
|
MULTICA_BACKEND_IMAGE=ghcr.io/multica-ai/multica-backend
|
|
MULTICA_WEB_IMAGE=ghcr.io/multica-ai/multica-web
|
|
|
|
# Email
|
|
# Two delivery options - only one needs to be configured:
|
|
#
|
|
# Option A: Resend (SaaS, recommended for cloud deployments)
|
|
# Set RESEND_API_KEY to a key from resend.com and verify your sending domain there.
|
|
# For local/dev use, leave RESEND_API_KEY empty - codes print to stdout. To
|
|
# accept a fixed local code, also set MULTICA_DEV_VERIFICATION_CODE above
|
|
# (ignored when APP_ENV=production).
|
|
RESEND_API_KEY=
|
|
RESEND_FROM_EMAIL=noreply@multica.ai
|
|
#
|
|
# Option B: SMTP relay (for self-hosted / on-premise deployments)
|
|
# Takes priority over Resend when SMTP_HOST is set.
|
|
# Supports unauthenticated relay (leave SMTP_USERNAME empty) and authenticated SMTP.
|
|
# Set SMTP_TLS_INSECURE=true only for private CA or self-signed certificates.
|
|
# SMTP_TLS controls the TLS mode:
|
|
# - unset / "starttls" (default): plaintext connect, upgrade via STARTTLS.
|
|
# - "implicit" (aliases: "smtps", "ssl"): TLS handshake on connect (SMTPS).
|
|
# Required by providers that only offer port 465 and do not advertise
|
|
# STARTTLS (e.g. Aliyun enterprise mail). Auto-enabled when SMTP_PORT=465
|
|
# and SMTP_TLS is unset.
|
|
# SMTP_EHLO_NAME is the EHLO/HELO name announced to the relay. Defaults to the
|
|
# machine hostname; set a real FQDN when a strict relay (e.g. Google Workspace
|
|
# smtp-relay.gmail.com) rejects the default and the connection drops as an EOF.
|
|
SMTP_HOST=
|
|
SMTP_PORT=25
|
|
SMTP_USERNAME=
|
|
SMTP_PASSWORD=
|
|
SMTP_TLS_INSECURE=false
|
|
SMTP_TLS=
|
|
SMTP_EHLO_NAME=
|
|
|
|
# Google OAuth
|
|
# The web login page reads GOOGLE_CLIENT_ID from /api/config at runtime, so
|
|
# changing it only requires restarting the backend / compose stack. No web
|
|
# rebuild is needed.
|
|
GOOGLE_CLIENT_ID=
|
|
GOOGLE_CLIENT_SECRET=
|
|
# Derived by docker-compose.selfhost.yml / local scripts from FRONTEND_ORIGIN.
|
|
# Set explicitly only when your OAuth callback URL differs from local frontend.
|
|
GOOGLE_REDIRECT_URI=${FRONTEND_ORIGIN}/auth/callback
|
|
|
|
# S3 / CloudFront
|
|
# S3_BUCKET — bucket NAME only (e.g. "my-bucket"). Do NOT include the
|
|
# ".s3.<region>.amazonaws.com" suffix; the server builds the public URL
|
|
# from S3_BUCKET + S3_REGION. S3_REGION must match the bucket's real region.
|
|
S3_BUCKET=
|
|
S3_REGION=us-west-2
|
|
AWS_ACCESS_KEY_ID=
|
|
AWS_SECRET_ACCESS_KEY=
|
|
# AWS_ENDPOINT_URL — optional S3-compatible endpoint (MinIO, RustFS, R2, etc.).
|
|
# For internal Docker/VPC hosts such as http://rustfs:9000, leave
|
|
# ATTACHMENT_DOWNLOAD_MODE=auto or set proxy explicitly so browsers/CLI do
|
|
# not need direct access to the object store.
|
|
AWS_ENDPOINT_URL=
|
|
ATTACHMENT_DOWNLOAD_MODE=auto
|
|
ATTACHMENT_DOWNLOAD_URL_TTL=30m
|
|
CLOUDFRONT_KEY_PAIR_ID=
|
|
CLOUDFRONT_PRIVATE_KEY_SECRET=multica/cloudfront-signing-key
|
|
CLOUDFRONT_PRIVATE_KEY=
|
|
CLOUDFRONT_DOMAIN=
|
|
# COOKIE_DOMAIN — optional Domain attribute on session + CloudFront cookies.
|
|
# Leave empty for single-host deployments (localhost, LAN IP, or a single
|
|
# hostname) — session cookies become host-only, which is what the browser
|
|
# wants. Only set it when the frontend and backend sit on different
|
|
# subdomains of one registered domain (e.g. ".example.com"). Do NOT set it
|
|
# to an IP address: RFC 6265 forbids IP literals in the cookie Domain
|
|
# attribute and browsers silently drop such cookies.
|
|
COOKIE_DOMAIN=
|
|
|
|
# AUTH_TOKEN_TTL — auth token lifetime. Accepts Go duration strings (e.g.
|
|
# "8760h", "720h30m") or plain integer seconds.
|
|
# Default: 2592000 (30 days). Self-hosted deployments on trusted networks can
|
|
# set a longer value to reduce re-authentication frequency.
|
|
# Note: longer TTL = longer exposure window if a cookie is leaked.
|
|
# AUTH_TOKEN_TTL=2592000
|
|
|
|
# Local file storage (fallback when S3_BUCKET is not set)
|
|
LOCAL_UPLOAD_DIR=./data/uploads
|
|
# Derived by Makefile / local scripts from the backend port.
|
|
# Set explicitly only when uploads are served through a different public URL.
|
|
# LOCAL_UPLOAD_BASE_URL=http://localhost:8080
|
|
|
|
# Security
|
|
# Comma-separated list of allowed origins for CORS and WebSocket connections.
|
|
# Defaults to localhost dev origins when unset.
|
|
# Example: CORS_ALLOWED_ORIGINS=https://app.multica.ai,https://staging.multica.ai
|
|
CORS_ALLOWED_ORIGINS=
|
|
|
|
# ==================== Rate limiting (optional Redis) ====================
|
|
# Per-IP fixed-window rate limiter on the public auth endpoints
|
|
# (/auth/send-code, /auth/verify-code, /auth/google). Backed by Redis.
|
|
# When REDIS_URL is unset the limiter is a no-op (fail-open) and the
|
|
# backend logs "rate limiting disabled: REDIS_URL not configured" at
|
|
# startup. The same REDIS_URL is reused by the realtime fan-out hub,
|
|
# the PAT cache, and the daemon-token cache.
|
|
# REDIS_URL=redis://localhost:6379/0
|
|
# Max requests per IP per minute. Defaults are 5 for send-code/google
|
|
# and 20 for verify-code.
|
|
# RATE_LIMIT_AUTH=5
|
|
# RATE_LIMIT_AUTH_VERIFY=20
|
|
# Comma-separated CIDRs whose X-Forwarded-For the auth limiter is
|
|
# allowed to trust. Empty (default) = never trust XFF, only RemoteAddr.
|
|
# REQUIRED behind a reverse proxy — otherwise every real user shares
|
|
# the proxy IP and the whole deployment lands in one bucket, turning
|
|
# /auth/send-code into 5 req/min site-wide. Use e.g. "127.0.0.1/32,::1/128"
|
|
# for same-host Caddy/Nginx, or the CDN's published ranges for ALB/CF.
|
|
# This is a separate list from MULTICA_TRUSTED_PROXIES above (which
|
|
# governs the autopilot webhook limiter).
|
|
# RATE_LIMIT_TRUSTED_PROXIES=
|
|
|
|
# Realtime metrics endpoint (/health/realtime) access control. See MUL-1342.
|
|
# When unset, the endpoint only serves direct loopback (127.0.0.1 / ::1)
|
|
# callers with no forwarding headers and returns 404 to everything else —
|
|
# safe for local dev. Any deployment behind a reverse proxy (Caddy / Nginx
|
|
# terminating TLS in front of localhost:8080) MUST set this token, since
|
|
# proxied requests look like loopback at the Go layer; with no token, those
|
|
# requests are refused with 404. Pass the token as
|
|
# `Authorization: Bearer <token>`.
|
|
# REALTIME_METRICS_TOKEN=
|
|
|
|
# GitHub App integration (Settings → GitHub "Connect GitHub")
|
|
# Both must be set for the Connect button to enable and for webhooks to be
|
|
# accepted; leave empty to disable the integration. See docs/github-integration.
|
|
# GITHUB_APP_SLUG is the tail of https://github.com/apps/<slug>.
|
|
GITHUB_APP_SLUG=
|
|
GITHUB_WEBHOOK_SECRET=
|
|
# Optional: GitHub App identity for App-authenticated REST calls. When set,
|
|
# the setup callback enriches the installation row with the real account
|
|
# login (org / user name) immediately after install. When unset, the row
|
|
# is created with the "unknown" placeholder and the next `installation`
|
|
# webhook from GitHub overwrites it — set both to skip that interim flash.
|
|
# GITHUB_APP_ID is the numeric "App ID" shown on the App's settings page.
|
|
# GITHUB_APP_PRIVATE_KEY is the full PEM block (including BEGIN/END lines)
|
|
# generated under "Private keys" on that same page; preserve newlines.
|
|
GITHUB_APP_ID=
|
|
GITHUB_APP_PRIVATE_KEY=
|
|
|
|
# Lark / Feishu bot integration (Settings → Integrations "Bind to Lark")
|
|
# Off until MULTICA_LARK_SECRET_KEY is set — a base64-encoded 32-byte key
|
|
# that encrypts each Bot's app secret at rest. Leave empty to disable.
|
|
# Generate one with: openssl rand -base64 32
|
|
MULTICA_LARK_SECRET_KEY=
|
|
# Mainland 飞书 and international Lark are auto-detected per installation
|
|
# (at QR scan) and served side by side — LEAVE THESE EMPTY for normal use.
|
|
# They are optional deployment-wide overrides that force EVERY installation
|
|
# onto one host (a proxy, a mock for tests, or a single-cloud staging
|
|
# setup); HTTP drives outbound Open Platform API calls, CALLBACK the inbound
|
|
# long-conn bootstrap. NOTE: if you previously ran international Lark by
|
|
# setting these to https://open.larksuite.com, the server relabels your
|
|
# existing installs to region=lark on first boot after upgrade, so you can
|
|
# clear these afterwards. See docs/lark-bot-integration.
|
|
MULTICA_LARK_HTTP_BASE_URL=
|
|
MULTICA_LARK_CALLBACK_BASE_URL=
|
|
|
|
# Frontend
|
|
# Leave empty — auto-derived from page origin in browser, set by Makefile for local dev.
|
|
# NEXT_PUBLIC_API_URL also feeds the Next.js SSR proxy when explicitly set.
|
|
NEXT_PUBLIC_API_URL=
|
|
NEXT_PUBLIC_WS_URL=
|
|
|
|
# Remote API (optional) — set to proxy local frontend to a remote backend
|
|
# Leave empty to use local backend (localhost:8080)
|
|
# REMOTE_API_URL=https://multica-api.copilothub.ai
|
|
|
|
# ==================== Self-hosting: Control Signups (fixes #930) ====================
|
|
# Set to "false" to completely disable new user signups (recommended for private instances)
|
|
ALLOW_SIGNUP=true
|
|
# The web UI reads ALLOW_SIGNUP from /api/config at runtime, so toggling this
|
|
# only requires restarting the backend / compose stack — not rebuilding web.
|
|
# It is not hot-reloaded.
|
|
|
|
# Optional: Only allow emails from these domains (comma-separated)
|
|
ALLOWED_EMAIL_DOMAINS=
|
|
|
|
# Optional: Only allow these exact email addresses (comma-separated)
|
|
ALLOWED_EMAILS=
|
|
|
|
# Set to "true" to disable workspace creation for every caller on this
|
|
# instance (#3433). Operators usually leave this unset, bootstrap the
|
|
# shared workspace, then flip this to "true" and restart so subsequent
|
|
# users join only via invitations and the entire deployment is visible to
|
|
# the platform admin. The web UI reads this from /api/config at runtime,
|
|
# so toggling requires a backend restart but not a frontend rebuild.
|
|
DISABLE_WORKSPACE_CREATION=
|
|
|
|
# ==================== Analytics (PostHog) ====================
|
|
# Product analytics events feed the acquisition → activation → expansion funnel.
|
|
# Leave POSTHOG_API_KEY empty for local dev / self-hosted instances; the server
|
|
# will run a no-op analytics client and ship nothing. See docs/analytics.md.
|
|
POSTHOG_API_KEY=
|
|
POSTHOG_HOST=https://us.i.posthog.com
|
|
# Optional override for the `environment` PostHog event property.
|
|
# Defaults from APP_ENV and normalizes to production / staging / dev.
|
|
ANALYTICS_ENVIRONMENT=
|
|
# Force the no-op client even when POSTHOG_API_KEY is set (CI / opt-out).
|
|
ANALYTICS_DISABLED=
|