Environment Variables
TaskView is configured through environment variables set in the .env.taskview file (or passed directly to the Docker container). This page documents every available variable.
Database
These must match your PostgreSQL setup.
| Variable | Required | Default | Description |
|---|---|---|---|
DB_HOST | Yes | - | Database hostname. Use db when running in Docker Compose. |
DB_USER | Yes | - | Database username |
DB_PASSWORD | Yes | - | Database password |
DB_NAME | Yes | - | Database name |
DB_PORT | No | 5432 | Database port |
DB_POOL_MAX | No | 20 | Maximum Postgres connections per API worker (each worker opens its own pool) |
Connection budget: the total number of Postgres connections is roughly PM2_INSTANCES × DB_POOL_MAX. Keep it below the max_connections of your PostgreSQL (default 100), leaving ~10 connections of headroom for migrations and maintenance. The API logs a warning at startup when the estimate exceeds 80.
Application
| Variable | Required | Default | Description |
|---|---|---|---|
APP_PORT | No | 1401 | Port the API server listens on |
PM2_INSTANCES | No | 2 | Number of API worker processes (PM2 cluster mode). Accepts a number or max (one worker per CPU core). When using max, set DB_POOL_MAX yourself so the connection budget above still fits. |
APP_URL | Yes | https://app.taskview.tech | Full URL of the web app (e.g. https://tasks.company.com). Used for OAuth redirects and email links. |
API_PUBLIC_URL | No | - | Public URL of the API server as external systems see it (e.g. https://api.company.com). Used to build the SSO callback/ACS URL and the SCIM endpoint shown in organization settings. Set it when the API runs behind a reverse proxy — otherwise those URLs are derived from the browser's address and may show an internal host that your IdP cannot reach. The server refuses to start if the value is not a valid http(s) URL. |
LOG_LEVEL | No | info in production, debug otherwise | Minimum level written to the log files in logs/: fatal, error, warn, info, debug, trace or silent. An unknown value falls back to the default and is reported at startup. |
TRUST_PROXY | No | false | Set when running behind a reverse proxy so X-Forwarded-Proto/X-Forwarded-For are honoured (correct https URLs, real client IP). Use the number of proxies in front of the app (1 for a single Caddy/nginx), or an IP/subnet list (10.0.0.0/8, uniquelocal). Leave unset for direct access. Avoid true (trusts any hop, allows header spoofing). |
Web app
Unlike everything else on this page, this variable is set on the web app container (taskview-webapp), not in .env.taskview.
| Variable | Required | Default | Description |
|---|---|---|---|
TASKVIEW_API_URL | No | - | Pins the API server URL for the web app (e.g. https://api.company.com). When set, the "Select server" section disappears from the login page and the app always talks to this API. When unset, users pick the API server on the login page themselves. |
Authentication
| Variable | Required | Default | Description |
|---|---|---|---|
JWT_SIGN | Yes | - | Secret key for signing JWT tokens. Use a long random string. |
ACCESS_LIFE_TIME | No | 1d | How long access tokens are valid. Examples: 1h, 1d, 7d |
REFRESH_LIFE_TIME | No | 2d | How long refresh tokens are valid |
SESSION_MAX_AGE_DAYS | No | 90 | How many days a sign-in session lasts. Refreshing tokens does not extend it: after this many days since sign-in the user has to sign in again. Must be a positive integer; anything else stops the server at startup. |
JWT_ALG | No | HS256 | JWT signing algorithm |
AUTH_LOGIN_METHODS | No | all enabled | Comma-separated list of login methods to offer: magic-link, password, sso, social. Disabled methods disappear from the login page and their API endpoints return 403. The API refuses to start if the list contains a typo or disables every method. |
LOGIN_CODE_HOURLY_LIMIT | No | 5 | Maximum sign-in codes one account can request by email per hour. Each code also stops working after 5 wrong attempts. Must be a positive integer; anything else stops the server at startup. |
PASSWORD_CHANGE_CONFIRMATION | No | email | How account password changes are confirmed: email — a confirmation code is sent to the user's email (requires SMTP); password — the user confirms with their current password (works without SMTP, recommended for installs without a mail server). |
ALLOW_PUBLIC_REGISTRATION | No | true | Set to false to close the instance: strangers can no longer create accounts — the registration endpoint returns 403, and magic-link / social sign-in stop auto-creating users. Emails invited to an organization or project can still sign in and get their account created on first login. |
OAUTH_DYNAMIC_REGISTRATION | No | false | Whether AI clients may register themselves as OAuth clients (RFC 7591) when connecting to the MCP server. Off by default: the registration endpoint returns 403 and cloud clients such as ChatGPT or Claude cannot start an OAuth connection. Set to true to let them connect. Turning it back off later does not affect apps that are already connected. |
SSO_TRUSTED_DOMAINS | No | empty | Comma-separated email domains that skip DNS/HTTP ownership checks for SSO (air-gapped / closed-network installs). Example: company.com,corp.local. On a public instance leave this unset so every org must prove it owns the domain. |
SSO_ALLOW_PRIVATE_URLS | No | false | The server fetches SAML IdP metadata (the Sync button) and the SSO domain-ownership file. Those URLs may only point to public addresses: loopback, private, link-local and cloud-metadata ranges are refused, also when a public-looking host name resolves to them, and redirects are not followed. Set to true on a self-hosted install whose identity provider (Keycloak, Authentik) lives on the same private network. The value must be true or false; anything else stops the server at startup. |
WEBHOOKS_ALLOW_PRIVATE_URLS | No | false | Outgoing webhooks may only target public addresses: loopback, private (10/8, 172.16/12, 192.168/16), link-local and cloud-metadata ranges are refused when a webhook is saved and again on every delivery, and redirects are never followed. Set to true on a self-hosted install whose receivers (n8n, CI, internal bots) live on the same private network. Never enable this on a public multi-tenant instance. The value must be true or false; anything else stops the server at startup. |
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"Closing an instance: how ALLOW_PUBLIC_REGISTRATION=false works
By default anyone who can reach your instance can create an account — through the registration endpoint, or simply by entering an email on the login page (magic-link and social sign-in create the account on first login). Set ALLOW_PUBLIC_REGISTRATION=false to close the instance: from that moment accounts are created by invitation only.
Who can still get an account on a closed instance. TaskView invitations are stored by email, before any account exists. An email is considered invited — and its owner can sign in and get an account created on first login — if it appears in any of these places:
- Organization members — added via organization settings (or provisioned through SCIM)
- Project collaborators — invited to a project by an existing user
Everyone else is rejected: the registration endpoint returns 403, magic-link refuses to send a code to an unknown email, and social sign-in redirects back to the login page with an error. Existing accounts are not affected in any way — the flag only controls the creation of new ones.
SSO is not blocked by this flag. Signing in through a SAML/OIDC provider still provisions accounts, because an identity provider is configured by the administrator and is itself a controlled channel — your IdP decides who gets in.
The value must be true or false. Any other value (a typo like Flase, 0, no) stops the server at startup with a clear error instead of silently leaving the instance open.
SMTP (Email)
Required for password recovery, email confirmation, and invitation notifications. Without SMTP, these features won't work, but everything else functions normally.
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_HOST | No | - | SMTP server hostname |
SMTP_PORT | No | 465 | SMTP port |
SMTP_USERNAME | No | - | SMTP login |
SMTP_PASSWORD | No | - | SMTP password |
SMTP_ENCRYPTION | No | ssl | ssl or tls |
SMTP_FROM_NAME | No | TaskView | Sender name in emails |
SMTP_FROM_EMAIL | No | - | Sender email address |
INVITE_EMAIL_ENABLED | No | false | Set to true to email a person when they are invited to a project. The email is localized (English/Russian) by the inviter's browser language and links to the project. Requires SMTP; the value must be true or false — anything else stops the server at startup. |
INVITE_EMAIL_HOURLY_LIMIT | No | 30 | Maximum invite emails one user may trigger per hour. On top of this cap, the same address is never emailed about the same project more than once per 24 hours. Must be a positive integer. |
Encryption
Required for SSO (SAML/OIDC) and GitHub/GitLab integrations. Secrets are encrypted at rest using AES-256-GCM.
| Variable | Required | Default | Description |
|---|---|---|---|
ENCRYPTION_KEY | Yes | - | 32-byte hex string (64 characters). Required for SSO and integrations. |
Generate a key:
openssl rand -hex 32
What is encrypted:
- SSO: SAML certificates, SAML signing keys, OIDC client secrets
- Integrations: GitHub/GitLab OAuth tokens, webhook secrets
GitHub Integration
For connecting GitHub repositories. See GitHub, GitLab & Gitea Setup for a step-by-step guide.
| Variable | Required | Default | Description |
|---|---|---|---|
GITHUB_INTEGRATION_CLIENT_ID | No | - | OAuth App client ID |
GITHUB_INTEGRATION_CLIENT_SECRET | No | - | OAuth App client secret |
GITHUB_INTEGRATION_CALLBACK_URL | No | - | OAuth callback URL |
GITHUB_BASE_URL | No | https://github.com | Override for GitHub Enterprise |
GITHUB_API_URL | No | https://api.github.com | Override for GitHub Enterprise API |
GitLab Integration
For connecting GitLab repositories.
| Variable | Required | Default | Description |
|---|---|---|---|
GITLAB_INTEGRATION_CLIENT_ID | No | - | OAuth App client ID |
GITLAB_INTEGRATION_CLIENT_SECRET | No | - | OAuth App client secret |
GITLAB_INTEGRATION_CALLBACK_URL | No | - | OAuth callback URL |
GITLAB_BASE_URL | No | https://gitlab.com | Override for self-hosted GitLab |
GITLAB_API_URL | No | https://gitlab.com/api/v4 | Override for self-hosted GitLab API |
Gitea Integration
For connecting Gitea repositories.
| Variable | Required | Default | Description |
|---|---|---|---|
GITEA_INTEGRATION_CLIENT_ID | No | - | OAuth2 application client ID |
GITEA_INTEGRATION_CLIENT_SECRET | No | - | OAuth2 application client secret |
GITEA_INTEGRATION_CALLBACK_URL | No | - | OAuth callback URL |
GITEA_BASE_URL | No | https://gitea.com | Override for self-hosted Gitea |
GITEA_API_URL | No | {GITEA_BASE_URL}/api/v1 | Override for self-hosted Gitea API |
Messaging Integrations (Telegram / Slack)
For delivering task notifications to messengers. See Telegram & Slack Setup for a step-by-step guide.
| Variable | Required | Default | Description |
|---|---|---|---|
TELEGRAM_BOT_TOKEN | No | - | Bot token from @BotFather |
TELEGRAM_BOT_USERNAME | No | - | Bot username without @ (used to build deep-links) |
TELEGRAM_WEBHOOK_SECRET | No | - | Secret verified against Telegram's X-Telegram-Bot-Api-Secret-Token header on inbound updates |
SLACK_CLIENT_ID | No | - | Slack app client ID |
SLACK_CLIENT_SECRET | No | - | Slack app client secret |
SLACK_CALLBACK_URL | No | - | OAuth callback URL (must match the Slack app redirect URL) |
SLACK_SIGNING_SECRET | No | - | Slack request signing secret — verifies inbound slash commands / interactivity (/task, Done/Assign buttons) |
SLACK_API_BASE_URL | No | https://slack.com/api | Override the Slack Web API base (testing / enterprise proxy) |
SLACK_AUTHORIZE_URL | No | https://slack.com/oauth/v2/authorize | Override the Slack OAuth authorize URL |
SLACK_WEBHOOK_PREFIX | No | https://hooks.slack.com/ | Override the incoming-webhook URL prefix (used to detect legacy webhook connections) |
Notifications
Optional configuration for real-time and push notification delivery.
Centrifugo (real-time WebSocket notifications)
Required for in-app real-time notification delivery. Without Centrifugo, notifications are still saved to the database but will not appear instantly in the browser. Users will see them on the next page load.
| Variable | Required | Default | Description |
|---|---|---|---|
CENTRIFUGO_API_URL | No | - | Internal Centrifugo API URL (e.g. http://centrifugo:8000) |
CENTRIFUGO_API_KEY | No | - | Centrifugo API key for server-to-server communication. Must match http_api.key in Centrifugo config. |
CENTRIFUGO_TOKEN_SECRET | No | - | Secret for generating client connection tokens (HMAC). Must match client.token.hmac_secret_key in Centrifugo config. |
CENTRIFUGO_PUBLIC_URL | No | - | Full WebSocket URL for browser clients (e.g. wss://api.example.com/centrifugo/connection/websocket) |
Firebase Cloud Messaging (mobile push notifications)
Required for native push notifications on iOS and Android. Without Firebase, push notifications are silently skipped.
| Variable | Required | Default | Description |
|---|---|---|---|
FIREBASE_CREDENTIALS_PATH | No | - | Path to Firebase service account JSON file (e.g. ./firebase-credentials.json) |
Centrifugo configuration file
Centrifugo v6 uses a nested JSON config. Create centrifugo/config.json:
{
"client": {
"allowed_origins": ["https://app.example.com"],
"token": {
"hmac_secret_key": "your_centrifugo_token_secret"
}
},
"channel": {
"namespaces": [
{
"name": "personal",
"presence": false,
"join_leave": false,
"history_size": 0,
"history_ttl": "0s"
}
]
},
"http_api": {
"key": "your_centrifugo_api_key"
}
}
http_api.keymust matchCENTRIFUGO_API_KEYin.env.taskviewclient.token.hmac_secret_keymust matchCENTRIFUGO_TOKEN_SECRETin.env.taskviewclient.allowed_originsshould list your frontend domain(s). Use["*"]only for development.
Nginx WebSocket proxy
To serve Centrifugo through your existing HTTPS domain, add to your nginx server block:
location /centrifugo/ {
proxy_pass http://localhost:8000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
Then set CENTRIFUGO_PUBLIC_URL=wss://api.example.com/centrifugo/connection/websocket.
Files (task attachments)
Uploaded files are stored outside PostgreSQL; the database keeps only metadata (name, size, SHA-256 checksum, which provider holds the bytes). Two providers are supported and both may be configured at the same time, which is how an instance is migrated from one to the other without downtime.
Attachments are off until a provider is configured: FILE_STORAGE_PROVIDER must name a provider and all of that provider's variables must be set. Otherwise the API starts normally, logs a warning with what is missing, hides the upload buttons in the web app and answers uploads with 503. There is no default directory on purpose: in Docker an unmounted directory lives inside the container and is lost when the container is recreated.
| Variable | Required | Default | Description |
|---|---|---|---|
FILE_STORAGE_PROVIDER | To enable files | - | Provider that receives new uploads: local or s3. Its settings must be present, otherwise attachments stay disabled. |
FILE_MAX_SIZE_MB | No | 10 | Maximum size of one uploaded file in megabytes. |
FILE_STORAGE_LOCAL_DIR | For local | - | Directory for the local provider. In Docker mount it as a volume, e.g. /taskview/files:/usr/src/app/data/files. |
FILE_STORAGE_S3_BUCKET | For s3 | - | Bucket name. |
FILE_STORAGE_S3_REGION | For s3 | - | Region, e.g. us-east-1. Any value works for MinIO. |
FILE_STORAGE_S3_ENDPOINT | No | - | Custom endpoint for S3-compatible storage (MinIO, Cloudflare R2, Backblaze B2). Leave empty for AWS. |
FILE_STORAGE_S3_ACCESS_KEY | For s3 | - | Access key id. |
FILE_STORAGE_S3_SECRET_KEY | For s3 | - | Secret access key. |
FILE_STORAGE_S3_FORCE_PATH_STYLE | No | false | Set to true for MinIO and most self-hosted S3-compatible servers. |
Storage quotas
Quotas limit how much each organization can store. They are on by default: every organization gets 100 MB unless configured otherwise.
| Variable | Required | Default | Description |
|---|---|---|---|
FILE_QUOTA_MODE | No | enforce | enforce - uploads over the quota are rejected with 507. off - no quotas. An unknown value means off and is reported at startup. |
FILE_QUOTA_ORGANIZATION_MB | No | 100 | Default quota of every organization, in megabytes. |
Each organization, personal ones included, has its own quota: FILE_QUOTA_ORGANIZATION_MB by default, or the value of tv_auth.organizations.file_quota_mb (in megabytes) when it is set. The API never changes this column; set it from billing or by hand:
UPDATE tv_auth.organizations SET file_quota_mb = 20480 WHERE id = <organization id>;
Usage is a live sum of the files in the organization's projects, so deleting a file frees its space at once. Lowering a quota below current usage only blocks new uploads; nothing is deleted. The quota is never exceeded, parallel uploads included: an upload is cut as soon as it passes the remaining space, and each file is recorded under a lock on its organization after a final check.
Encryption at rest
TaskView does not encrypt file contents itself: the API writes the bytes it received and the database keeps only metadata. Encrypt at the storage layer instead; the API does not need to know about it.
s3- enable server-side encryption on the bucket (SSE-S3 or SSE-KMS on AWS, SSE on MinIO). Uploads and downloads keep working unchanged.local- keepFILE_STORAGE_LOCAL_DIRon an encrypted disk or volume (for example LUKS on Linux). Back up the encryption key separately from the data.
This protects the files on a lost disk, a leaked backup or an exposed bucket. Anyone who can call the API with file permissions still reads the files normally - that part is governed by the project roles file_can_view and file_can_manage.
Moving files between providers
- Add the settings of the new provider while
FILE_STORAGE_PROVIDERstill points to the old one. - Run the migration inside the API container (the CLI ships in the image next to the server bundle):
docker exec -it <api-container> npm run files:migrate -- --from local --to s3
Use--to local --from s3for the reverse direction and add--goal <projectId>to move a single project. From a source checkout the same command ispnpm --filter taskview-ce-api-server build:files-migrate && pnpm --filter taskview-ce-api-server files:migrate -- --from local --to s3. The API keeps running; each file is copied, verified against its checksum, switched in the database and then removed from the old provider. - Switch
FILE_STORAGE_PROVIDERto the new provider and restart the API. - Run the same command once more to pick up files uploaded between steps 2 and 3. A report of
migrated: 0means the move is complete. - Remove the old provider's variables.
Full example
Here's a complete .env.taskview file for a production deployment:
DB_HOST="db"
DB_USER="taskview_db_user"
DB_PASSWORD="password"
DB_NAME="taskview"
DB_PORT=5432
# Postgres connections per API worker (this is the default)
DB_POOL_MAX=20
APP_PORT=1401
# API worker processes (this is the default). Accepts a number or "max" (one worker per CPU core).
# Keep PM2_INSTANCES x DB_POOL_MAX below the Postgres max_connections limit (default 100).
PM2_INSTANCES=2
JWT_ALG="HS256"
JWT_SIGN="secret"
ACCESS_LIFE_TIME="3d"
REFRESH_LIFE_TIME="9d"
# Login methods offered on the login page (unset = all enabled)
#AUTH_LOGIN_METHODS="magic-link,password,sso,social"
# Max sign-in codes one account can request by email per hour
#LOGIN_CODE_HOURLY_LIMIT=5
# Days a sign-in session lasts, however often its tokens are refreshed
#SESSION_MAX_AGE_DAYS=90
# Password change confirmation: "email" (code by email, needs SMTP) or "password" (no SMTP needed)
#PASSWORD_CHANGE_CONFIRMATION="email"
# Air-gapped SSO: skip DNS/HTTP domain proof for these email domains
#SSO_TRUSTED_DOMAINS="company.com,corp.local"
# Let webhooks target receivers on the private network (self-hosted only)
#WEBHOOKS_ALLOW_PRIVATE_URLS=false
SMTP_HOST=smtp
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_ENCRYPTION=tls
SMTP_FROM_NAME=TaskView
SMTP_FROM_EMAIL=
# Your domain
APP_URL="https://app.taskview.tech"
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
#You domain
GOOGLE_CALLBACK_URL="https://api.taskview.tech/module/auth/provider/google/callback"
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GITHUB_CALLBACK_URL="https://api.taskview.tech/module/auth/provider/github/callback"
APPLE_CLIENT_ID=""
APPLE_TEAM_ID=""
APPLE_KEY_ID=""
APPLE_KEY_LOCATION="/usr/src/app/AuthKey.p8"
# Your domain
APPLE_CALLBACK_URL="https://api.taskview.tech/module/auth/provider/apple/callback"
#integrations
GITHUB_INTEGRATION_CLIENT_ID=
GITHUB_INTEGRATION_CLIENT_SECRET=
GITHUB_INTEGRATION_CALLBACK_URL=https://api.taskview.tech/module/integrations/oauth/github/callback
GITLAB_INTEGRATION_CLIENT_ID=
GITLAB_INTEGRATION_CLIENT_SECRET=
GITLAB_INTEGRATION_CALLBACK_URL=https://api.taskview.tech/module/integrations/oauth/github/callback
ENCRYPTION_KEY=
# Messaging integrations
TELEGRAM_BOT_TOKEN=
TELEGRAM_BOT_USERNAME=
TELEGRAM_WEBHOOK_SECRET=
# SLACK_CLIENT_ID=
# SLACK_CLIENT_SECRET=
# SLACK_SIGNING_SECRET=
# Notifications (optional)
# FIREBASE_CREDENTIALS_PATH=./firebase-credentials.json
# CENTRIFUGO_API_URL=http://centrifugo:8000
# CENTRIFUGO_API_KEY=your_centrifugo_api_key
# CENTRIFUGO_TOKEN_SECRET=your_centrifugo_token_secret
# CENTRIFUGO_PUBLIC_URL=wss://api.example.com/centrifugo/connection/websocket
# Files (attachments stay disabled until a provider is configured; mount the directory as a volume)
FILE_STORAGE_PROVIDER=local
FILE_STORAGE_LOCAL_DIR=/usr/src/app/data/files
# FILE_MAX_SIZE_MB=10
Telegram & Slack Setup
Connect Telegram and Slack to TaskView to receive task notifications. Personal direct messages and project-wide group channels, with instance-level bot credentials, OAuth, signed inbound webhooks, and SHA-256 hashed binding tokens.
Authentication
Configure authentication in TaskView - email/password, email/code, OAuth with GitHub, Google, and Apple Sign In. JWT session management, password recovery, and account deletion for your self-hosted instance.
