Skip to content

Configuration

Configuration system

A configuration management system enables type checking and automatic value merging in the following priority order (highest wins):

txt
default variables → configuration file variables → environment variables

Environment variables are parsed to extract only keys with specific prefixes (improving security). Keys are split by __ (double underscore) to reconstruct the nested configuration object. Arrays must be passed as JSON strings.

PrefixNamespaceDescription
SERVER__serverServer host, port, domain
DB__dbDatabase connection URL and pool
AUTH__authBetterAuth secret, base URL, Redis
OIDC__oidcOIDC federation settings
BOOTSTRAP__bootstrapInitial admin user credentials
MODULES__modulesFeature module toggles
PLATFORM__platformPlatform-level app configuration

Configuration files:

  • Development: apps/api/config-example.json
  • Production: /app/config.json (mounted at runtime)

API

The API is built on top of Fastify with the following plugins already configured:

The API is fully typed and validated through Zod schemas, with a custom type-safe routing system providing full type safety for request/response validation and automatic OpenAPI documentation generation.

Notes:

  • Swagger UI is available at http(s)://<api_domain>/swagger-ui. It exposes two spec sources: Application API (Fastify routes) and Auth API (BetterAuth — select from the top-right dropdown). A standalone Scalar reference UI for auth is also available at /api/v1/auth/reference.
  • A getApiClient function is exported from the shared package, providing a typed fetch client for other apps and packages that consume the API.

Database

Prisma is used as the ORM, providing type-safe database access and migration management. The schema is split across multiple files:

  • prisma/schema.prisma — generator and datasource configuration
  • prisma/auth.prisma — BetterAuth-managed models (user, session, account, org, member, invitation, apiKey, jwks)
  • prisma/audit.prisma — audit log model

The codebase is structured to allow migration to other ORMs (e.g. Drizzle, Mongoose) by replacing the prisma/ folder and updating the resources/**/queries.ts files.

Endpoints

MethodPathAuthDescription
GET/api/v1/healthzPublicStartup probe
GET/api/v1/readyzPublicReadiness probe (checks DB)
GET/api/v1/livezPublicLiveness probe
GET/api/v1/versionPublicCurrent API version
ANY/api/v1/auth/*Public / AuthBetterAuth catch-all
GET/api/v1/auth/referencePublicInteractive OpenAPI reference (Scalar UI)
GET/api/v1/projectsAuthenticatedList own projects (admin: all projects)
GET/api/v1/projects/:idAuthenticatedGet own project by ID (admin: any)
POST/api/v1/projectsAuthenticatedCreate project (owner = current user)
PUT/api/v1/projects/:idAuthenticatedUpdate own project (admin: any)
DELETE/api/v1/projects/:idAuthenticatedDelete own project (admin: any)
GET/api/v1/themePublicGet platform theme configuration
PUT/api/v1/themeAdminUpdate platform theme configuration
GET/api/v1/configPublicGet app configuration (e.g. registration)
PUT/api/v1/configAdminUpdate app configuration
GET/api/v1/auditAudit:ReadQuery audit logs (admin: all; org admin: own org)
GET/api/v1/organizations/:organizationId/auditAudit:ReadQuery audit logs scoped to a specific organization
GET/api/v1/admin/organizationsAdminList all organizations with member counts
GET/api/v1/admin/organizations/:idAdminGet organization by ID with members and invitations
GET/api/v1/admin/api-keysAdminList all API keys
GET/api/v1/admin/api-keys/:idAdminGet API key by ID
GET/api/v1/admin/users/:idAdminGet user by ID with organizations, projects, and API keys
PUT/api/v1/api-keys/:idAuthenticatedUpdate own API key (name, permissions, metadata)

Ownership rules: regular users can only read, update, or delete projects they own (ownerId matches their session user ID). Admins bypass ownership checks. The ownerId is set automatically from the session on creation — it is not a caller-supplied field.

Environment variables

Database

VariableDescriptionDefault / Example
DB__URLPrimary PostgreSQL connection URL (read-write). Injected from the CNPG-generated secret in Kubernetes.postgresql://user:pass@host:5432/db
DB__READ_URLOptional read-replica URL (e.g. CNPG's -ro service). Pure read queries (findMany, findUnique, count) are routed here, offloading the primary. Falls back to DB__URL.postgresql://user:pass@host-ro:5432/db (optional)
DB__POOL__MAXMaximum connections in the primary (db) pg.Pool per API pod. Size for (maxReplicas × pool.max) + BetterAuth + headroom < max_connections.15
DB__POOL__RO_MAXMaximum connections in the read-replica (dbRo) pool per API pod. Can be higher than DB__POOL__MAX since replicas handle no write traffic.25

Server

VariableDescriptionDefault / Example
SERVER__HOSTServer listen address127.0.0.1
SERVER__PORTServer listen port8081
SERVER__DOMAINPublic host:port used in Swagger URLs127.0.0.1:8081
SERVER__BASE_PATHBase path prefix for all routes (set to "" on a dedicated API sub-domain)/api
SERVER__RATE_LIMIT__MAXGlobal Fastify rate-limit ceiling per IP per minute1000
SERVER__RATE_LIMIT__AUTH_MAXPer-IP rate limit for routes under /auth/* per minute20

Auth, OIDC & Bootstrap

VariableDescriptionDefault / Example
AUTH__SECRET256-bit secret for session signing(required in production)
AUTH__BASE_URLPublic API base URLhttp://localhost:8081
AUTH__TRUSTED_ORIGINSComma-separated list of trusted CORS originshttp://localhost:3000
AUTH__REDIS__URLStandalone Redis URL for session secondary storageredis://redis:6379 (optional)
AUTH__REDIS__SENTINEL_URLSComma-separated host:port pairs for Sentinel mode — takes precedence over REDIS__URLredis:26379,redis-2:26379 (optional)
AUTH__REDIS__SENTINEL_MASTERSentinel master name (required with REDIS__SENTINEL_URLS)mymaster
AUTH__REDIS__PASSWORDRedis node password for both standalone and Sentinel modes(optional)
AUTH__REDIS__SENTINEL_PASSWORDSentinel node password — falls back to AUTH__REDIS__PASSWORD when not set(optional)
AUTH__RATE_LIMIT__ENABLEDEnable BetterAuth's per-IP rate limiter (separate from Fastify's). Disable for load testing.true
AUTH__RATE_LIMIT__WINDOWBetterAuth rate-limit window in seconds10
AUTH__RATE_LIMIT__MAXBetterAuth max requests per window per IP (defaults to 100; built-in stricter rules apply to /sign-in* etc.)100
OIDC__ENABLEDEnable OIDC federation (e.g. Keycloak)false
OIDC__CLIENT_IDOIDC client IDtemplate-monorepo-ts
OIDC__CLIENT_SECRETOIDC client secret
OIDC__ISSUEROIDC realm issuer URL (internal, used for server-to-server calls)http://keycloak:8080/realms/<realm>
OIDC__PUBLIC_URLOIDC realm URL reachable by the browser (falls back to OIDC__ISSUER when empty)
OIDC__MAP_ROLESSync OIDC realm roles → BetterAuth rolefalse
OIDC__MAP_GROUPSSync OIDC groups → BetterAuth rolefalse
OIDC__MAP_ORG_ROLESSync OIDC org roles → BetterAuth org member rolefalse
OIDC__ORG_ROLE__PREFIXPrefix used to extract org role from OIDC token claimsorg-
OIDC__ORG_ROLE__DEFAULTDefault org member role when none is mappedmember
BOOTSTRAP__EMAILBootstrap admin emailadmin@example.com (optional)
BOOTSTRAP__PASSWORDBootstrap admin password(optional)
MODULES__AUDIT__ENABLEDEnable the audit modulefalse
MODULES__AUDIT__RETENTION_DAYSDays to retain audit log entries (0 = keep forever)0
PLATFORM__APP_NAMEPlatform display nameTemplate Monorepo TS
PLATFORM__DOCUMENTATION_URLDocumentation URL shown in Swagger externalDocs
PLATFORM__ENABLE_REGISTRATIONAllow new user self-registrationtrue
PLATFORM__ALLOW_ORGANIZATION_CREATIONAllow users to create organizationstrue
PLATFORM__MAINTENANCE_MODEPut the platform in maintenance mode (read-only)false
PLATFORM__MAX_ORGANIZATIONS_PER_USERMaximum organizations a user can belong to (null = unlimited)null
PLATFORM__MAX_PROJECTS_PER_ORGMaximum projects per organization (null = unlimited)null

Observability

VariableDescriptionDefault
OTEL_SERVICE_NAMEService name reported in traces and metricsapi
OTEL_EXPORTER_OTLP_ENDPOINTOTel Collector OTLP endpointhttp://otel-collector:4318
OTEL_SDK_DISABLEDDisable the OTel SDK entirelyfalse

OTel is automatically disabled in test environments (NODE_ENV=test).

Logging

VariableDescriptionDefault
LOG_LEVELMinimum log level (trace, debug, info, warn, error, fatal, silent)silent in test, debug in development, info in production
NODE_ENVControls log format and default levelproduction

Logging is provided by the @template-monorepo-ts/logger package (Pino-based). In development, logs are pretty-printed; in production, they are JSON-formatted for machine consumption. OpenTelemetry trace context (traceId, spanId) is automatically injected into every log entry when a span is active.

MCP Server

VariableDescriptionDefault
TMTS_SERVER_URLBase URL of the API server(required)
TMTS_TOKENBearer token for session-based auth
TMTS_API_KEYAPI key for key-based auth
TMTS_TRANSPORTTransport mode: stdio (local) or http (network)stdio
TMTS_HTTP_HOSTHTTP listen host (only when TMTS_TRANSPORT=http)0.0.0.0
TMTS_HTTP_PORTHTTP listen port (only when TMTS_TRANSPORT=http)3100

Web

VariableScopeDescriptionDefault
VITE_API_URLDev (Vite)Browser-side API URL (include base path, e.g. /api)http://localhost:8081/api
VITE_APP_VERSIONDev (Vite)App version display in dev modedev
API_PROXY_TARGETDev (Vite)Vite proxy target for /api (Docker network address)http://localhost:8081
API_URLProd (Docker)API URL injected via envsubst (include base path)http://api:8080/api
APP_VERSIONProd (Docker)App version injected via envsubst (set by CI/CD)dev

Enterprise proxy

If the API server needs to reach external services (Keycloak, OAuth providers) through an HTTP proxy, set the standard proxy environment variables:

VariableDescriptionDefault
HTTP_PROXYProxy URL for HTTP requests (e.g. http://proxy.corp.example.com:3128)
HTTPS_PROXYProxy URL for HTTPS requests (e.g. http://proxy.corp.example.com:3128)
NO_PROXYComma-separated list of hosts/domains to bypass (e.g. localhost,.local)

Bun natively routes all fetch() calls (used by BetterAuth, Keycloak OIDC, and health probes) through the proxy. Internal TCP connections (PostgreSQL, Redis) and the OTel HTTP exporter (which targets a local collector) are unaffected.

Docker Compose — proxy variables are passed through from the host environment automatically (no value assignment needed).

Kubernetes (Helm) — set the variables under global.env in your values file:

yaml
global:
  env:
    HTTP_PROXY: "http://proxy.corp.example.com:3128"
    HTTPS_PROXY: "http://proxy.corp.example.com:3128"
    NO_PROXY: "localhost,127.0.0.1,.cluster.local,.svc"