Skip to content

Configuration

All configuration is via environment variables. Copy .env.example to .env; the defaults match infra/docker-compose.dev.yml.

VarDefaultNotes
DATABASE_URLpostgresql://memoturn:memoturn@localhost:5433/memoturn?schema=publicHost port is 5433 in dev to avoid clashing with other local Postgres
PRISMA_POOL_SIZE10Prisma (metadata) connection pool size per replica.
PRISMA_CONNECT_TIMEOUT_MS10000How long to wait for a pooled Postgres connection before failing the query.
VarDefaultNotes
TELEMETRY_ENGINEdorisdoris (the scale engine, default) or postgres — the small-install tier that keeps telemetry in the DATABASE_URL Postgres, schema telemetry, with no Doris containers at all. Needs a pgvector-enabled image (the shipped compose files use pgvector/pgvector:pg16). Sizing + how to switch engines: Deployment → Telemetry engine.
TELEMETRY_DATABASE_URLDATABASE_URLOptional separate Postgres for the postgres engine’s telemetry tables.
TELEMETRY_PG_SCHEMAtelemetrySchema holding the postgres engine’s tables + migration ledger.

Applies when TELEMETRY_ENGINE=doris (the default).

VarDefaultNotes
DORIS_HOSTlocalhostDoris FE host
DORIS_PORT9030FE MySQL-protocol port
DORIS_HTTP_PORT8030FE HTTP port
DORIS_USERroot
DORIS_PASSWORDemptyEmpty in dev; required in production (the prod compose files include a one-shot doris-setup service that sets the root password)
DORIS_DBmemoturn
DORIS_FE_XMX4096mFE JVM heap cap, applied by the prod compose file. Raise on hosts with memory to spare (see Deployment → Doris sizing).
DORIS_BE_MEM_LIMIT6G dev / 40% prodBE process memory cap (absolute like 8G, or a % of host memory). Raise if analytical queries fail with MEM_ALLOC_FAILED.
TELEMETRY_STREAM_LOADfalseSet true/1 to switch worker inserts to Doris Stream Load (HTTP) for higher throughput. In-network deploys just work (the FE 307-redirects to a BE).
DORIS_STREAM_LOAD_HOSTDORIS_HOSTOverride for a host-run worker that should load a BE directly instead of going through the FE redirect.
DORIS_STREAM_LOAD_PORT8030FE HTTP port by default; point at a BE webserver (8040) to load it directly.
DORIS_STREAM_LOAD_TIMEOUT_MS60000Per-call Stream Load timeout so a wedged BE can’t pin an ingest worker slot forever.
DORIS_REPLICATION_NUM1Replicas per tablet for NEW tables/partitions (${REPLICATION_NUM} in DDL, the migrations ledger). Raise on multi-BE clusters; existing tables are changed with bun run telemetry:repartition -- --set-replication N.
DORIS_POOL_SIZE10Doris FE connection pool size per replica.
DORIS_CONNECT_TIMEOUT_MS10000Connect timeout to the FE.
DORIS_QUERY_TIMEOUT_S60Server-side query_timeout set on every pooled session — a runaway scan is killed on the FE instead of pinning a pool slot.
TELEMETRY_PG_POOL_SIZE10Postgres-tier telemetry pool size per replica (TELEMETRY_ENGINE=postgres).
TELEMETRY_PG_CONNECT_TIMEOUT_MS10000Connect timeout for that pool.
TELEMETRY_PG_STATEMENT_TIMEOUT_MS60000statement_timeout on every telemetry session.
VarDefaultNotes
REDIS_URLredis://localhost:6380Host port 6380 in dev
VarDefaultNotes
WORKER_CONCURRENCY10Ingest worker concurrency
INGEST_LOCK_DURATION_MS60000BullMQ lock for an ingest job; a job that outlives it is marked stalled (2 stalls → failed → DLQ). Raise if inserts routinely exceed a minute.
LONG_JOB_LOCK_DURATION_MS600000BullMQ lock for experiment / evaluator-backfill jobs (minutes-long, LLM-bound).
DLQ_ALERT_DEPTH1000When the dead-letter queue reaches this depth the worker logs an error-level line (“failing systemically”). 0 disables.
TELEMETRY_MAX_RETENTION_DAYS0 (off)Instance-wide retention ceiling: every project — with or without a policy — is swept at min(policy, ceiling) across the telemetry store, the blob event log, and the Postgres state mirror.
WORKER_SHUTDOWN_TIMEOUT_MS570000Drain budget on SIGTERM: how long the worker waits for in-flight jobs (experiments/backfills run for minutes) before force-exiting. Keep it under the orchestrator’s grace period (Helm worker.terminationGracePeriodSeconds, compose stop_grace_period).
WORKER_PORT3002Worker /health (liveness), /ready (readiness — pings every datastore), and /metrics (JSON, or Prometheus text with Accept: text/plain / ?format=prometheus) HTTP endpoint
WORKER_HOST127.0.0.1Bind host for the worker health/metrics server. Loopback by default — /metrics is unauthenticated and leaks queue depths and per-project evaluator names. Set 0.0.0.0 only for cross-host probes on a trusted network.
WORKER_METRICS_URLhttp://127.0.0.1:3002/metricsWhere the API fetches worker metrics for the ingest-health panel. Set it when the API and worker run on different hosts/pods; fetch failures degrade gracefully (workerReachable: false).
STATE_RETENTION_HOURS72Hours a mutable-entity *State row stays in Postgres after its last update before the hourly prune drops it (Doris keeps full history).
EXPERIMENT_CONCURRENCY2Concurrent experiment jobs per worker process. Kept low on purpose — each job fans out over dataset items and must not starve ingest.
EVAL_BACKFILL_CONCURRENCY1Concurrent evaluator-backfill jobs per worker process. Each one issues judge calls over a whole trace window, so it stays serial by default.
EXPERIMENT_ITEM_CONCURRENCY4Dataset items executed in parallel within one experiment run.
MAINTENANCE_CONCURRENCY4Maintenance-queue concurrency, so the per-minute alert tick isn’t blocked behind a long daily sweep (retention/export/embeddings). Each job type is lock-guarded.
GUARDRAIL_EVALUATOR_TIMEOUT_MS3000Per-check timeout for the LLM-backed guardrails (evaluator guards + the built-in restricted-topic/toxicity model guards). They sit on the request path, so a slow judge must not hang the caller — each fails open on timeout.
ALERT_ANOMALY_BUCKETS12Number of trailing time buckets used as the baseline for anomaly-type alert rules.
ALERT_ANOMALY_MIN_BASELINE5Minimum baseline events before an anomaly rule can fire (suppresses noise on quiet projects).
EMBEDDING_PROJECTION_DAYS30Lookback window for the daily embedding-projection reduction.
EMBEDDING_PROJECTION_MAX_POINTS5000Cap on points per projection run.
EMBEDDING_PROJECTION_CLUSTERS8k-means cluster count for the projection.
EMBEDDING_PROJECTION_METHODumapReduction algorithm: umap (seeded, deterministic) or pca. Sets below 30 points always use PCA.
SIMILAR_TRACES_SEED_CAP8Max seed vectors compared per “find similar traces” query (each adds a distance term to the Doris SQL).
VarDefault
BLOB_ENDPOINThttp://localhost:9000
BLOB_REGIONus-east-1
BLOB_BUCKETmemoturn
BLOB_ACCESS_KEY_IDmemoturn
BLOB_SECRET_ACCESS_KEYmemoturn123
BLOB_FORCE_PATH_STYLEtrue
VarDefaultNotes
API_PORT3001Hono API
CONSOLE_PORT3000Vite SPA
MEMOTURN_API_URLhttp://localhost:3001API target for the console dev proxy
RATE_LIMIT_PER_MINUTE600Per-project global request rate limit (requests/minute). ON by default; 0 disables it (per-key limits still apply). Also applied to the remote MCP endpoint once its principal resolves.
INGEST_EVENTS_PER_MINUTE60000Per-project ingest event budget (a POST can carry 1000 events). ON by default; 0 disables.
API_DOCS_PUBLICtrueSet false to put /docs + /openapi.json behind auth.
API_KEY_DEFAULT_EXPIRY_DAYSunsetDefault lifetime for API keys minted without an explicit expiresInDays (unset = never expire).
PLAYGROUND_MAX_TOKENS32768Ceiling on maxTokens for a single playground/assistant completion (these spend the project’s provider key)
MCP_RATE_LIMIT_PER_MINUTE120Per-IP budget for the remote MCP endpoint (/v1/mcp/:projectId). Unlike the project limiter it defaults on — the route runs a credential lookup before auth resolves, so unauthenticated clients must not get unthrottled tries. 0 disables.
INGEST_MAX_EVENT_BYTES1048576Per-event size cap on /v1/ingest (serialized). Over-limit events are rejected individually in the 207 body; large payloads belong in /v1/media.
INGEST_MAX_JSON_DEPTH32Per-event nesting cap — protects the worker’s recursive masking/offload walks.
API_REQUEST_TIMEOUT_MS60000Wall-clock budget per non-streaming /v1 request; past it the API answers 504 (JSON) instead of holding the connection. SSE and MCP routes are exempt.
SSE_MAX_STREAMS_PER_PROJECT20Cap on concurrently open SSE streams (live tail, playground/assistant streaming) per project, shared across replicas via Redis; the 21st gets 429. 0 disables.
LOG_LEVELinfoMinimum level emitted by the API’s and worker’s structured logs (debug/info/warn/error). warn silences the per-request access line on a busy install.
RATE_LIMIT_TRUSTED_PROXIES1Number of trusted reverse proxies in front of the API, used to derive the real client IP from the right of X-Forwarded-For (a spoofed XFF prefix can’t evade per-IP limits). The shipped Caddy deploy is one proxy; set 0 if the API is directly internet-exposed.
API_METRICS_TOKENunsetEnables GET /metrics (request counts, status classes, per-route latency percentiles). Unset → 404; set → requires Authorization: Bearer <token>. JSON by default; Prometheus text exposition with Accept: text/plain (what Prometheus sends) or ?format=prometheus. GET /ready (public) is the readiness probe — 200 only when Postgres, Redis, the telemetry store, and the blob bucket all answer; GET /health / /v1/health stay pure liveness.

Production startup guard: in production (NODE_ENV=production) the API and worker refuse to start if BETTER_AUTH_SECRET, ENCRYPTION_KEY, or AUTH_TRUSTED_ORIGINS are missing, shorter than 16 characters, or set to a known development placeholder. Generate fresh values with openssl rand -base64 48.

VarDefaultNotes
BETTER_AUTH_SECRETdev placeholderRequired in production — signs session cookies and tokens. Use openssl rand -base64 48.
AUTH_BASE_URLhttp://localhost:3001Better Auth base URL
CONSOLE_PUBLIC_URLfirst AUTH_TRUSTED_ORIGINS entryPublic origin of the console, used for links in emails (mentions, sign-in, invites). Defaults to the first AUTH_TRUSTED_ORIGINS entry, which is already the console origin — set this only when they differ. Deliberately not AUTH_BASE_URL: that is the API origin (:3001). A localhost value means “no public URL”, so notification emails ship without a link rather than a dead one.
AUTH_TRUSTED_ORIGINShttp://localhost:3000Required in production — comma-separated console origins for CORS + auth.
ENCRYPTION_KEYdev placeholderRequired in production — AES-256-GCM key for provider API keys stored at rest. Independent of BETTER_AUTH_SECRET. Rotating this invalidates all stored provider keys (they must be re-entered in Settings → Providers).
ENCRYPTION_KEYSunsetComma-separated key ring for rotation — first entry is ACTIVE (used to encrypt), the rest only decrypt. Procedure: ENCRYPTION_KEYS=new,old → restart → bun run rotate-secretsENCRYPTION_KEYS=new. Ciphertexts are v2.<keyId>.… (scrypt-derived AES-256-GCM); pre-v2 ciphertexts still decrypt and are rewritten by the rotation.
MCP_LOGIN_PAGE<first AUTH_TRUSTED_ORIGINS>/loginConsole sign-in page the remote-MCP OAuth 2.1 flow (Better Auth @better-auth/oauth-provider plugin) redirects unauthenticated users to. Override only if the console login lives elsewhere.
MCP_CONSENT_PAGE<first AUTH_TRUSTED_ORIGINS>/consentConsole consent page where the OAuth flow asks the signed-in user to approve the client’s requested scopes.
VarDefaultNotes
AUTH_DISABLE_PASSWORD_SIGNUPunsetSet true to disable new email/password signups (existing password logins still work). Meant for hosted/IdP-only deployments; leave unset for self-host so the first admin can register without SMTP or an IdP.
AUTH_REQUIRE_EMAIL_VERIFICATIONunsetSet true to require a verified email before sign-in (needs a working email transport). Default off so self-host accounts aren’t locked out.
AUTH_MIN_PASSWORD_LENGTH12Minimum length for new passwords (existing shorter passwords still sign in).
AUTH_HIBP_DISABLEDunsetThe breached-password check (k-anonymity, api.pwnedpasswords.com) is on by default and fails closed — signup/password-change return 500 when the service is unreachable. Airgapped/offline installs must set true.
AUTH_SIGNIN_MAX_PER_15M10Password sign-in attempts per client IP per 15 minutes (brute-force guard, on top of the generic 30/min auth window). 2FA code verification has fixed sub-limits (10 TOTP, 5 backup codes / 15 min).
AUTH_OAUTH_REGISTER_MAX_PER_HOUR20Unauthenticated OAuth dynamic client registrations per IP per hour (remote MCP clients).
AUTH_COOKIE_CACHE_MAX_AGE300Session cookie cache lifetime (seconds) — getSession is served from a short-lived signed cookie instead of a Postgres query. Revocations/bans take up to this long to bite on issued cookies.
AUTH_COOKIE_CACHE_DISABLEDunsetSet true to disable the session cookie cache entirely (every getSession hits Postgres).
AUTH_ORG_MEMBERSHIP_LIMIT10000Max members per organization.
AUTH_ORG_INVITATION_LIMIT1000Max pending invitations per organization.
AUTH_IP_HEADERSunsetComma-separated header(s) carrying the real client IP behind a proxy/CDN (first match wins), e.g. cf-connecting-ip or x-real-ip. Unset, the auth rate limiter trusts x-forwarded-for, which is spoofable when clients can reach the origin directly.
SSO_ADMIN_GROUPSemptyComma-separated IdP groups/roles that map a federated user to org admin on auto-join (matched against the groups/roles/role claims). Empty → everyone joins as member.
SUPERADMIN_USER_IDSemptyComma-separated user IDs that always pass platform-admin authorization (list/ban users, impersonate), independent of the user.role column. Keep empty on self-host unless you need it.
PASSKEY_RP_IDderivedWebAuthn relying-party ID (registrable domain, no scheme/port). Defaults derive from the first AUTH_TRUSTED_ORIGINS; set only when the console is served from a custom domain.
PASSKEY_ORIGINderivedFull origin users register passkeys from. Must match the console origin.
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETunsetEnable Google sign-in (button appears only when both are set). Callback: ${AUTH_BASE_URL}/auth/callback/google.
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETunsetEnable GitHub sign-in. Callback: ${AUTH_BASE_URL}/auth/callback/github.

Powers the email alert channel and auth flows (password reset, org invitations, email verification). The transport is auto-selected: an explicit EMAIL_TRANSPORT, else Resend when RESEND_API_KEY is set, else SMTP when SMTP_CONNECTION_URL or SMTP_HOST is set, else disabled. When disabled, auth emails are logged to stderr in development so flows stay testable without a mail server.

VarDefaultNotes
EMAIL_TRANSPORTautoForce resend or smtp instead of auto-selection.
EMAIL_FROMmemoturn <alerts@memoturn.local>From address for all outbound mail.
ALERT_EMAIL_FROMunsetLegacy fallback for the From address — used only when EMAIL_FROM is unset. Prefer EMAIL_FROM.
RESEND_API_KEYunsetResend HTTP API key (no SMTP server needed).
SMTP_CONNECTION_URLunsetSingle connection URL (smtp:// / smtps:// / ses://<region>); wins over the discrete vars.
SMTP_HOST / SMTP_PORT / SMTP_SECURE / SMTP_USER / SMTP_PASSunsetDiscrete SMTP settings (used when no connection URL is set).
VarDefaultNotes
LLM_TIMEOUT_MS60000Wall-clock timeout for non-streaming provider calls (evaluator/experiment judges — a hung provider would otherwise wedge a shared worker slot).
LLM_STREAM_TIMEOUT_MS300000Timeout for streaming calls (playground).
VarDefaultNotes
ALLOW_PRIVATE_WEBHOOK_TARGETSunsetSet to 1 to permit http:// and private/loopback webhook, automation, and analytics-sink URLs. Blocked by default in every environment to prevent SSRF (not just production). Useful for dev/LAN self-hosted targets. In production the API/worker refuse to start with it set unless ALLOW_PRIVATE_WEBHOOK_TARGETS_ACK=1 is also set.
ALLOW_PRIVATE_WEBHOOK_TARGETS_ACKunsetProduction-only acknowledgement that ALLOW_PRIVATE_WEBHOOK_TARGETS=1 is intentional (see above).
VarDefaultNotes
MEMOTURN_BASE_URLhttp://localhost:3001API base used by SDKs
MEMOTURN_PUBLIC_KEYpk-mt-devMatches the dev key from bun run seed
MEMOTURN_SECRET_KEYsk-mt-dev

bun run seed creates the default organization, project, API key, and admin user. In development the credentials are the well-known dev defaults; in production the script refuses to run unless ALLOW_SEED=1 is set, at which point it generates random credentials and prints them once.

VarDefaultNotes
ALLOW_SEEDunsetSet to 1 to allow bun run seed in NODE_ENV=production. Without it the script exits with an error (the dev credentials are public knowledge).
SEED_ADMIN_EMAILadmin@memoturn.devOverride the seeded admin email. In production a random value is generated unless this is set.
SEED_ADMIN_PASSWORDmemoturn-dev-123Override the seeded admin password. In production a random value is generated unless this is set.

Off by default and inert on a normal install. When enabled, a visitor who signs in with an email that has no organization gets a throwaway sandbox provisioned automatically — their own organization + project, seeded with generated telemetry — which is hard-deleted after DEMO_TTL_DAYS. Used to run the public demo; see the worker’s sandbox-prune cron.

VarDefaultNotes
DEMO_MODEunsettrue/1 enables per-visitor sandbox provisioning, the sandbox seeder, and the daily prune cron. Leave unset for every normal deployment.
DEMO_TTL_DAYS7Sandbox lifetime. The prune cron hard-deletes telemetry, blob objects, the organization (Prisma cascade), and the visitor’s user row.
DEMO_MAX_SANDBOXES500Cap on concurrently-active sandboxes; signups past it are refused.
DEMO_SEED_DAYS3Days of backdated demo telemetry generated per sandbox.
DEMO_SEED_TRACES_PER_DAY15Traces per day per sandbox — keep small; every sandbox pays this ingest cost.
DEMO_MEMBER_ROLEviewerRole the visitor gets. viewer is read-only (every mutating route is gated), which is what stops a public sandbox from ingesting, spending on the playground, or minting API keys.
DEMO_FINALIZE_DELAY_MS120000How long the finalize job waits after the seed batches are submitted, so async ingest can drain before the sign-in link is emailed. Raise it if visitors land on a half-empty dashboard.
DEMO_START_RATE_LIMIT_PER_MINUTE10Per-IP cap on POST /v1/demo/start, the unauthenticated pre-provision route. Honors RATE_LIMIT_TRUSTED_PROXIES when resolving the client IP.
SANDBOX_CONCURRENCY2Worker concurrency for the sandbox seed queue.
VITE_GA_MEASUREMENT_IDunsetGA4 measurement id baked into the demo’s console bundle at image build (compose build arg, not runtime env). Unset ships zero analytics code — leave it unset on self-host installs. Page views are sent with query strings stripped, so magic-link tokens never reach analytics, and Consent Mode v2 keeps analytics storage denied for EEA/UK/CH visitors.
VarDefaultNotes
CONSOLE_URLhttp://localhost:3000Console base URL for bun run screenshots (the docs screenshot generator). Dev tooling only — not read by any service.

For the security-relevant subset of these variables organized as a go-live checklist, see the hardening guide.