Environment Variables

Complete reference for TaskView environment variables - database connection, JWT authentication, OAuth providers, SMTP email, GitHub/GitLab integration, encryption, and CORS configuration for your self-hosted Docker deployment.

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.

VariableRequiredDefaultDescription
DB_HOSTYes-Database hostname. Use db when running in Docker Compose.
DB_USERYes-Database username
DB_PASSWORDYes-Database password
DB_NAMEYes-Database name
DB_PORTNo5432Database port
DB_POOL_MAXNo20Maximum 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

VariableRequiredDefaultDescription
APP_PORTNo1401Port the API server listens on
PM2_INSTANCESNo2Number 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_URLYeshttps://app.taskview.techFull URL of the web app (e.g. https://tasks.company.com). Used for OAuth redirects and email links.
API_PUBLIC_URLNo-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_LEVELNoinfo in production, debug otherwiseMinimum 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_PROXYNofalseSet 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.

VariableRequiredDefaultDescription
TASKVIEW_API_URLNo-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

VariableRequiredDefaultDescription
JWT_SIGNYes-Secret key for signing JWT tokens. Use a long random string.
ACCESS_LIFE_TIMENo1dHow long access tokens are valid. Examples: 1h, 1d, 7d
REFRESH_LIFE_TIMENo2dHow long refresh tokens are valid
SESSION_MAX_AGE_DAYSNo90How 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_ALGNoHS256JWT signing algorithm
AUTH_LOGIN_METHODSNoall enabledComma-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_LIMITNo5Maximum 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_CONFIRMATIONNoemailHow 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_REGISTRATIONNotrueSet 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_REGISTRATIONNofalseWhether 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_DOMAINSNoemptyComma-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_URLSNofalseThe 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_URLSNofalseOutgoing 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.
Generate a strong JWT secret: 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.

Note that any existing user can invite a collaborator to their project, and an invited email becomes eligible for an account. If your policy is stricter — "only administrators approve new accounts" — restrict who you give accounts to, since every user holds an invitation key to the instance.

SMTP (Email)

Required for password recovery, email confirmation, and invitation notifications. Without SMTP, these features won't work, but everything else functions normally.

VariableRequiredDefaultDescription
SMTP_HOSTNo-SMTP server hostname
SMTP_PORTNo465SMTP port
SMTP_USERNAMENo-SMTP login
SMTP_PASSWORDNo-SMTP password
SMTP_ENCRYPTIONNosslssl or tls
SMTP_FROM_NAMENoTaskViewSender name in emails
SMTP_FROM_EMAILNo-Sender email address
INVITE_EMAIL_ENABLEDNofalseSet 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_LIMITNo30Maximum 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.

VariableRequiredDefaultDescription
ENCRYPTION_KEYYes-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
If you change or lose the encryption key, all stored SSO configurations and integration tokens become unreadable. You'll need to reconfigure SSO providers and reconnect GitHub/GitLab integrations.

GitHub Integration

For connecting GitHub repositories. See GitHub, GitLab & Gitea Setup for a step-by-step guide.

VariableRequiredDefaultDescription
GITHUB_INTEGRATION_CLIENT_IDNo-OAuth App client ID
GITHUB_INTEGRATION_CLIENT_SECRETNo-OAuth App client secret
GITHUB_INTEGRATION_CALLBACK_URLNo-OAuth callback URL
GITHUB_BASE_URLNohttps://github.comOverride for GitHub Enterprise
GITHUB_API_URLNohttps://api.github.comOverride for GitHub Enterprise API

GitLab Integration

For connecting GitLab repositories.

VariableRequiredDefaultDescription
GITLAB_INTEGRATION_CLIENT_IDNo-OAuth App client ID
GITLAB_INTEGRATION_CLIENT_SECRETNo-OAuth App client secret
GITLAB_INTEGRATION_CALLBACK_URLNo-OAuth callback URL
GITLAB_BASE_URLNohttps://gitlab.comOverride for self-hosted GitLab
GITLAB_API_URLNohttps://gitlab.com/api/v4Override for self-hosted GitLab API

Gitea Integration

For connecting Gitea repositories.

VariableRequiredDefaultDescription
GITEA_INTEGRATION_CLIENT_IDNo-OAuth2 application client ID
GITEA_INTEGRATION_CLIENT_SECRETNo-OAuth2 application client secret
GITEA_INTEGRATION_CALLBACK_URLNo-OAuth callback URL
GITEA_BASE_URLNohttps://gitea.comOverride for self-hosted Gitea
GITEA_API_URLNo{GITEA_BASE_URL}/api/v1Override 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.

VariableRequiredDefaultDescription
TELEGRAM_BOT_TOKENNo-Bot token from @BotFather
TELEGRAM_BOT_USERNAMENo-Bot username without @ (used to build deep-links)
TELEGRAM_WEBHOOK_SECRETNo-Secret verified against Telegram's X-Telegram-Bot-Api-Secret-Token header on inbound updates
SLACK_CLIENT_IDNo-Slack app client ID
SLACK_CLIENT_SECRETNo-Slack app client secret
SLACK_CALLBACK_URLNo-OAuth callback URL (must match the Slack app redirect URL)
SLACK_SIGNING_SECRETNo-Slack request signing secret — verifies inbound slash commands / interactivity (/task, Done/Assign buttons)
SLACK_API_BASE_URLNohttps://slack.com/apiOverride the Slack Web API base (testing / enterprise proxy)
SLACK_AUTHORIZE_URLNohttps://slack.com/oauth/v2/authorizeOverride the Slack OAuth authorize URL
SLACK_WEBHOOK_PREFIXNohttps://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.

VariableRequiredDefaultDescription
CENTRIFUGO_API_URLNo-Internal Centrifugo API URL (e.g. http://centrifugo:8000)
CENTRIFUGO_API_KEYNo-Centrifugo API key for server-to-server communication. Must match http_api.key in Centrifugo config.
CENTRIFUGO_TOKEN_SECRETNo-Secret for generating client connection tokens (HMAC). Must match client.token.hmac_secret_key in Centrifugo config.
CENTRIFUGO_PUBLIC_URLNo-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.

VariableRequiredDefaultDescription
FIREBASE_CREDENTIALS_PATHNo-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.key must match CENTRIFUGO_API_KEY in .env.taskview
  • client.token.hmac_secret_key must match CENTRIFUGO_TOKEN_SECRET in .env.taskview
  • client.allowed_origins should 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.

Both Centrifugo and Firebase are optional. If not configured, their respective channels are skipped. Notifications are always persisted to the database.

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.

VariableRequiredDefaultDescription
FILE_STORAGE_PROVIDERTo enable files-Provider that receives new uploads: local or s3. Its settings must be present, otherwise attachments stay disabled.
FILE_MAX_SIZE_MBNo10Maximum size of one uploaded file in megabytes.
FILE_STORAGE_LOCAL_DIRFor 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_BUCKETFor s3-Bucket name.
FILE_STORAGE_S3_REGIONFor s3-Region, e.g. us-east-1. Any value works for MinIO.
FILE_STORAGE_S3_ENDPOINTNo-Custom endpoint for S3-compatible storage (MinIO, Cloudflare R2, Backblaze B2). Leave empty for AWS.
FILE_STORAGE_S3_ACCESS_KEYFor s3-Access key id.
FILE_STORAGE_S3_SECRET_KEYFor s3-Secret access key.
FILE_STORAGE_S3_FORCE_PATH_STYLENofalseSet 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.

VariableRequiredDefaultDescription
FILE_QUOTA_MODENoenforceenforce - uploads over the quota are rejected with 507. off - no quotas. An unknown value means off and is reported at startup.
FILE_QUOTA_ORGANIZATION_MBNo100Default 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 - keep FILE_STORAGE_LOCAL_DIR on 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

  1. Add the settings of the new provider while FILE_STORAGE_PROVIDER still points to the old one.
  2. 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 s3 for the reverse direction and add --goal <projectId> to move a single project. From a source checkout the same command is pnpm --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.
  3. Switch FILE_STORAGE_PROVIDER to the new provider and restart the API.
  4. Run the same command once more to pick up files uploaded between steps 2 and 3. A report of migrated: 0 means the move is complete.
  5. 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