Configuration
Castor has two kinds of configuration:
- Environment variables: what the server needs before it can reach the database (database URL,
SECRET_KEY, ports …), validated with Zod inapps/api/src/config.tsand injected bydocker-compose.ymlin Docker deployments. See "Backend (API)" below - System settings: mail, file storage, upload limits, the AI model, the site URL, the sign-in lockout and security switches such as two-step verification, edited after signing in under System → Configuration → System settings and applied within seconds, without a restart. Environment variables can pin these too, see Configuration in System settings
Config files
On startup, the backend loads .env.<NODE_ENV> based on NODE_ENV:
apps/api/.env.<NODE_ENV>.env.<NODE_ENV>in the repo root
When both files set the same variable, the value in apps/api/ wins. Environment variables that are already set (for example by the shell or by compose) are never overridden by a file.
| File | Purpose | Committed |
|---|---|---|
apps/api/.env.example | Local development example | Yes |
.env.production.example (repo root) | Annotated example of every variable (reference for Docker / production) | Yes |
apps/api/.env.development | Local development config | No (gitignored) |
.env.production (repo root) | Docker deployment config, generated by setup.sh | No (gitignored) |
Never commit real secrets
.env.development and .env.production are gitignored. Don't put a real SECRET_KEY, database password or API key into an example file or commit it to the repo.
Backend (API)
Runtime and database
| Variable | Purpose | Default |
|---|---|---|
NODE_ENV | Runtime environment: development / test / production; any other value is treated as development | development |
PORT | Listening port | 5001 in development, 5002 in test, 5000 in production |
APP_NAME | Product name the server shows: the issuer in authenticator apps, the default mail sender and the test mail's subject, the AI assistant's introduction, the startup log. The web app's name is APP_NAME in apps/web/src/lib/brand.ts (see Starting a project) | Castor |
DEV_DATABASE_URL | Development database connection | postgresql://localhost/castor_kit |
TEST_DATABASE_URL | Test database connection; the tests read it from the shell or apps/api/.env.test | postgresql://localhost/castor_kit_test |
DATABASE_URL | Production database connection | postgresql://localhost/castor_kit |
MIGRATIONS_DIR | Migrations directory | Found automatically by searching upward for a drizzle/ directory |
Security and sessions
| Variable | Purpose | Default |
|---|---|---|
SECRET_KEY | Session encryption key; the cookie key is derived from it via HKDF | Built-in insecure default in development / test; required in production |
ADMIN_PASSWORD | Initial password of the admin account, used only when the account doesn't exist (pnpm seed:rbac -- --incremental --reset-admin-password, or in Docker node dist/reset-admin-password.js, applies it to an existing account) | admin123 in development / test; required in production |
SESSION_TTL_HOURS | Initial session lifetime (hours); can be changed later in System settings | 8 |
COOKIE_SECURE | The cookie's Secure flag: true / false forces it; leave empty to decide from the request protocol (set only over HTTPS) | Empty (auto) |
CORS_ORIGINS | Allowed cross-origin origins, comma-separated; also used as the Origin allowlist for the WebSocket handshake | Empty |
RATE_LIMIT_ENABLED | Per-IP rate limits; the limits themselves are set in System settings, see Account security & settings | true |
SETTINGS_ALLOW_PRIVATE_NETWORK | Whether the SMTP server, S3 endpoint and AI API URL in System settings, and webhook endpoints, may point at internal networks (127.0.0.1, 10.x, 192.168.x …); reserved addresses such as cloud metadata are never allowed. Addresses pinned by environment variables aren't restricted | true in development / test, false in production |
BODY_LIMIT | Maximum request body size (bytes); larger requests get 413 | 16777216 (16MB) |
Paths
| Variable | Purpose | Default |
|---|---|---|
WEB_DIST_DIR | Frontend build directory; the backend serves static files and the SPA from here | apps/web/dist |
DATA_DIR | Runtime data directory; the local driver stores uploads in its uploads/files/ by default | apps/api/data |
File storage directory and mail development mode
| Variable | Purpose | Default |
|---|---|---|
STORAGE_LOCAL_DIR | Directory of the "local disk" storage | <DATA_DIR>/uploads/files |
MAIL_DRIVER | Empty = send through the SMTP server in System settings; log prints mails to the backend log instead of sending (local development); none never sends | Empty |
Local-disk storage needs a persistent disk: Docker Compose mounts DATA_DIR as a volume; on platforms like Render that wipe the disk on redeploy, switch to S3-compatible storage (e.g. Cloudflare R2) in System settings, or with the STORAGE_DRIVER / S3_* variables where the page is read-only (demo mode). Files that nothing references are removed by the task scheduler 24 hours after upload, so they aren't cleaned up while no scheduler runs (ENABLE_TASK_SCHEDULER=false, or RUN_SCHEDULER_IN_WEB=false without a separate worker process).
Public demo
| Variable | Purpose | Default |
|---|---|---|
DEMO_MODE | Public demo mode: the login page shows the demo account with one-click sign-in; every write except sign-in, the component gallery, file uploads and marking notifications read returns 403 (system management is read-only and passwords can't be changed); login lockout counts per IP only; sample data is restored periodically | false |
DEMO_RESET_HOURS | How often the demo data is restored (hours). Checked at startup and then hourly; restores when the last restore is older than this. Run pnpm demo:reset to restore right away | 24 |
DEMO_AI_HOURLY_PER_IP | AI calls (AI chat, AI SQL generation, the AI assistant) allowed per IP per hour in demo mode; over the limit returns 429. Signed-out requests don't count | 20 |
DEMO_AI_DAILY | AI calls allowed per day for the whole site in demo mode; once used up, 429 for the rest of the day | 300 |
DEMO_AI_MAX_INPUT_CHARS | Max size of one AI request in demo mode (characters; for AI chat only the message text counts); larger requests return 400. Demo mode also caps the length of the model's reply | 4000 |
The demo data lives in apps/api/src/demo/fixtures.ts and the restore logic in apps/api/src/demo/reset.ts. A restore only touches the component gallery, announcements, dictionaries, scheduled tasks, notifications and logs — never accounts, roles or menus.
Scheduled tasks
| Variable | Purpose | Default |
|---|---|---|
ENABLE_TASK_SCHEDULER | Whether the task scheduler is enabled | true |
RUN_SCHEDULER_IN_WEB | Whether to run the scheduler inside the web process; when false, you need to run the worker process separately | false |
TASK_SCHEDULER_INTERVAL_SECONDS | Scheduler scan interval (seconds) | 20 |
TASK_SCHEDULER_LEASE_SECONDS | Task lease duration (seconds), used to prevent duplicate runs and reclaim stuck tasks | 1800 |
Boolean values 1, true, yes and on (case-insensitive) count as true.
AI Data Query
The AI model (API URL, key, model name) is configured in System settings, see below. This section only holds the read-only database connection used by AI Data Query:
| Variable | Purpose | Default |
|---|---|---|
AI_SQL_DATABASE_URL | Read-only connection used by AI Data Query; should point at a non-superuser read-only account | Falls back to the main database connection in development / test (still forced read-only). In production, if unset but POSTGRES_RO_PASSWORD is set, it is derived from DATABASE_URL (switched to the castor_kit_ro account); otherwise startup fails |
AI_SQL_STATEMENT_TIMEOUT_MS | Per-statement timeout for AI Data Query (milliseconds) | 5000 |
POSTGRES_RO_PASSWORD | Password of the read-only account castor_kit_ro; setup-once / init-ro-role use it to create the account, and skip that step if it's not set | Empty |
Apifox (used only by pnpm openapi:apifox)
| Variable | Purpose | Default |
|---|---|---|
APIFOX_PROJECT_ID | Apifox project ID | Empty |
APIFOX_ACCESS_TOKEN | Apifox access token | Empty |
APIFOX_API_VERSION | Apifox API version | 2024-03-28 |
Required in production
With NODE_ENV=production, the server refuses to start if any of these is missing:
SECRET_KEYADMIN_PASSWORDAI_SQL_DATABASE_URL, orPOSTGRES_RO_PASSWORD(the read-only connection is then derived from it andDATABASE_URL)
When deploying with docker-compose.yml, compose builds AI_SQL_DATABASE_URL for you, so you don't need to set it by hand.
Configuration in System settings
These are edited on the System settings page (viewing needs system_settings, saving system_settings_edit) and apply within seconds. The Mail, File storage and AI tabs have test buttons that try unsaved values.
- Passwords, the S3 secret key and the API key are stored encrypted with a key derived from
SECRET_KEY; the page only shows that they are set. After changingSECRET_KEY, enter them again - When the matching environment variable is set (and not empty), it wins: the setting becomes read-only on the page and names the variable. Useful for deployments managed entirely through environment variables; leave them unset to manage settings on the page
- To pin a setting in Docker, add the variable to
app.environmentindocker-compose.ymlas well as to.env.production - Saving and testing need an identity check within the last 10 minutes, and every save notifies all super admins; see How system settings are protected. In production, pin at least
APP_BASE_URLandSMTP_HOSTwith environment variables
Mail
| Setting | Environment variable | Default |
|---|---|---|
| Site URL (links in mails are built from it, never from the request's Host) | APP_BASE_URL | Empty |
| SMTP server | SMTP_HOST | Empty (no mail) |
| Port | SMTP_PORT | 587 |
| Encryption: auto / SSL/TLS / STARTTLS (auto = SSL/TLS on port 465) | SMTP_SECURE (true = SSL/TLS, false = STARTTLS) | Auto |
| Account / password | SMTP_USER / SMTP_PASSWORD | Empty |
Sender, e.g. Castor <noreply@example.com> | MAIL_FROM | The account |
Password reset by email can only be turned on once the SMTP server and the site URL are set, see Account security & settings.
File storage and uploads
| Setting | Environment variable | Default |
|---|---|---|
| Store in: local disk / S3-compatible storage (AWS S3, MinIO, Alibaba Cloud OSS, Tencent COS, Cloudflare R2) | STORAGE_DRIVER (local / s3) | Local disk |
| S3 endpoint (empty for AWS S3) | S3_ENDPOINT | Empty |
Region (auto for R2) | S3_REGION | us-east-1 |
| Bucket / access key / secret key (required for S3) | S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEY | Empty |
| Public URL (downloads redirect there; otherwise to a signed URL valid for about 10 minutes) | S3_PUBLIC_URL | Empty |
| Addressing: auto / path style / virtual-hosted (auto = path style when an endpoint is set) | S3_FORCE_PATH_STYLE (true / false) | Auto |
Max file size, also capped by BODY_LIMIT | UPLOAD_MAX_SIZE (bytes) | 10MB |
| Allowed file types; uploads are also checked for a file signature matching the extension | UPLOAD_ALLOWED_TYPES (comma-separated) | jpg,jpeg,png,gif,webp,pdf,txt,csv,doc,docx,xls,xlsx,ppt,pptx,zip |
Switching the storage only affects new uploads; existing files record where they live (including the bucket) and are still read from there. If new S3 endpoint or keys can't reach the original bucket, files stored there can no longer be read; the page says how many files are affected.
AI model
| Setting | Environment variable | Default |
|---|---|---|
| Service type: OpenAI-compatible / OpenAI / Anthropic / Google | AI_PROVIDER (openai-compatible / openai / anthropic / google) | OpenAI-compatible |
API URL: required for OpenAI-compatible (e.g. https://api.deepseek.com/v1); leave empty for the others to use the official API, or enter a proxy | AI_API_BASE | Empty |
| API key | AI_API_KEY | Empty |
| Model | AI_MODEL | Empty |
| Enable the AI assistant: the chat assistant in the bottom-right corner that looks data up as the signed-in user and changes it once approved, see AI assistant; needs a configured model | None (set on the page only) | Off |
AI Chat, AI Prompt Studio, AI Data Query and the AI assistant share these settings; calls go through the Vercel AI SDK without automatic retries. "OpenAI-compatible" covers every service with a /chat/completions API: DeepSeek, Qwen, Gemini's compatible endpoint, Ollama and more. When they are not set, those pages show a "not configured" notice; everything else is unaffected.
Sign-in lockout
| Setting | Environment variable | Default |
|---|---|---|
| Failed attempts before lockout (counted per IP and per username; per IP only in demo mode) | LOGIN_MAX_FAILURES | 10 |
| Lockout duration (minutes), also the window failures are counted in | LOGIN_LOCKOUT_MINUTES | 15 |
Other security settings (two-step verification, password reset, password rules, session lifetime, rate limits) are only on the page, see Account security & settings.
Frontend (Web)
The frontend has no runtime environment variables. Dev server behavior is defined in apps/web/vite.config.ts:
| Setting | Value |
|---|---|
| Dev port | 5173 |
| Proxy | /api → http://localhost:5001, /ws → ws://localhost:5001 (the port is API_PORT when set, e.g. API_PORT=5011 for a second checkout whose backend runs with PORT=5011) |
| Path alias | @ → apps/web/src |
In production, the backend serves the frontend build directly and requests go to same-origin /api, so no extra configuration is needed.
User preferences in the browser (theme, appearance, language, tabs bar) are stored in localStorage / sessionStorage; see Theme & layout.
Docker
Variables read by compose
Put these in .env.production and pass them in with docker compose --env-file .env.production ...:
| Variable | Purpose | Default |
|---|---|---|
POSTGRES_PASSWORD | Password of the PostgreSQL user castor_kit | Required |
SECRET_KEY | See above | Required |
ADMIN_PASSWORD | See above | Required |
POSTGRES_RO_PASSWORD | Password of the AI SQL read-only account | Required |
NPM_REGISTRY | npm registry used to install dependencies while building the image (build argument) | https://registry.npmmirror.com |
APP_PORT | Port mapped on the host (always 5000 inside the container) | 8080 (the config generated by setup.sh writes 5000) |
ENABLE_TASK_SCHEDULER | See above | true |
RUN_SCHEDULER_IN_WEB | See above. Defaults to true in compose, unlike the backend's own default | true |
SESSION_TTL_HOURS | See above | 8 |
COOKIE_SECURE | See above | Empty (auto) |
CORS_ORIGINS | See above | Empty |
RATE_LIMIT_ENABLED | See above | true |
AI_PROVIDER / AI_API_KEY / AI_API_BASE / AI_MODEL | Optional: pin the AI model settings (see AI model); leave empty to configure them in System settings | Empty |
COMPOSE_DB_VOLUME | Name of the database volume; can point to an existing volume | castor-kit_postgres_data |
COMPOSE_DATA_VOLUME | Name of the runtime data volume (mounted at DATA_DIR, holds uploads); can point to an existing volume | castor-kit_app_data |
Based on these, compose sets the following automatically:
| Variable | Value |
|---|---|
NODE_ENV | production |
DATABASE_URL | postgresql://castor_kit:<POSTGRES_PASSWORD>@db/castor_kit |
AI_SQL_DATABASE_URL | postgresql://castor_kit_ro:<POSTGRES_RO_PASSWORD>@db/castor_kit |
Other variables in .env.production (such as LOGIN_MAX_FAILURES) are not passed into the container automatically; add them under app.environment in docker-compose.yml if you need them.
Variables baked into the image
Set in the Dockerfile; you normally don't need to change them. The NPM_REGISTRY build argument defaults to https://registry.npmjs.org; builds through compose use the mainland-China mirror by default (see the table above).
| Variable | Value |
|---|---|
NODE_ENV | production |
PORT | 5000 |
WEB_DIST_DIR | /app/web |
DATA_DIR | /app/data |
MIGRATIONS_DIR | /app/drizzle |
Toolchain
| Variable | Purpose |
|---|---|
CASTOR_KIT_ROOT | Repo root used by the MCP Server; derived from its own location by default |
