Skip to main content

Environment Variables

Every variable the People Portal server reads, grouped by what it configures.

src/config/environment.ts reads .env, then .env.<NODE_ENV>, then .env.<NODE_ENV>.local. The first value found wins because the loader uses override: false. Anything already in the real process environment always wins.

For local development, run peopleportal/dev-stack/scripts/up.sh --write-env to create peopleportal/server/.env.development only if absent. It also prints the exact block for a server .env. Existing env files are never overwritten; an older .env can shadow these generated development values. See Local Development.

The dev stack uses PORTAL_PORT=3000 and WEBUI_PORT=5173 by default. For alternate ports, its env renderer emits PP_DEV_PORTAL_PORT and/or PP_DEV_WEBUI_PORT only for changed values. --write-env creates the UI overrides in peopleportal/webui/.env.development.local if absent. These variables configure Vite, not the server. Existing env files are preserved; remove stale overrides when returning to defaults. See the local development guide for every backing-service port variable and the override procedure.

Core​

VariableNotes
NODE_ENVdevelopment, test or production. Anything else throws. Default development
PORTListen port. Default 3000
PEOPLEPORTAL_BASE_URLPublic origin. Builds the OIDC redirect and post-logout URLs
PEOPLEPORTAL_WEBHOOK_URLOrigin Gitea calls back on. Usually the same as above
PEOPLEPORTAL_MONGO_URLMongoDB connection string
PEOPLEPORTAL_REDIS_URLCache. Optional: unset logs "running without a cache" and continues
PEOPLEPORTAL_TOKEN_SECRETSession signing secret

PEOPLEPORTAL_TOKEN_SECRET behaves differently by environment. In production it is required and the server throws without it. In development it generates an ephemeral one and warns, so sessions do not survive a restart.

Authentication​

VariableNotes
PEOPLEPORTAL_AUTHENTIK_ENDPOINTAuthentik base URL
PEOPLEPORTAL_AUTHENTIK_TOKENAdmin API token, sent as Bearer
PEOPLEPORTAL_OIDC_DSCVURLDiscovery document URL
PEOPLEPORTAL_OIDC_CLIENTIDClient id from the Authentik provider
PEOPLEPORTAL_OIDC_CLIENTSECRETClient secret

The dev stack uses http://localhost:9000 for Authentik and http://localhost:9000/application/o/people-portal/.well-known/openid-configuration for discovery. It generates the admin token and separate portal/Gitea clients; no TLS verification override is needed for local HTTP.

The app requests openid profile email people_portal offline_access, and reads pk, is_superuser and attributes from the custom scope. See OpenID Provider Configuration for the provider side, including what logout needs.

Gitea​

VariableNotes
PEOPLEPORTAL_GITEA_ENDPOINTGitea base URL
PEOPLEPORTAL_GITEA_TOKENAdmin token. Needs write:admin for system webhooks
PEOPLEPORTAL_GITEA_WEBHOOK_SECRETShared secret. Minimum 32 characters

The dev stack uses http://localhost:3001 for Gitea. Its generated webhook origin is http://host.docker.internal:3000, so container callbacks reach the host server on Linux and macOS.

The webhook secret is not optional once Gitea is enabled. GiteaHookSetup builds the Authorization: Bearer header from it before creating either hook, and throws below 32 characters, so a short value means zero hooks rather than partial ones. Inbound webhooks are rejected with 401 unless the header matches, compared with timingSafeEqual.

Slack and Discord​

VariableNotes
PEOPLEPORTAL_SLACK_BOT_TOKENBot token. SlackClient throws at construction if unset
PEOPLEPORTAL_SLACK_INVITE_URLWorkspace invite link surfaced to new members
PEOPLEPORTAL_DISCORD_BOT_TOKENOptional
PEOPLEPORTAL_DISCORD_SERVER_IDOptional

Discord degrades: with either missing, the client logs "Discord integration disabled" and the server starts normally. A bad token also degrades rather than crashing, which it used to do at import time.

Slack scopes, from the nine Web API methods used: chat:write, channels:manage, channels:read, users:read, users:read.email, plus groups:write and groups:read for private channels. users:read.email is requested separately from users:read and is easy to miss.

Email​

VariableNotes
PEOPLEPORTAL_SMTP_HOSTSMTP server
PEOPLEPORTAL_SMTP_PORT465 for SSL, 587 for STARTTLS; dev-stack Mailpit uses 1025
PEOPLEPORTAL_SMTP_SECUREtrue or false
PEOPLEPORTAL_SMTP_USERSMTP username
PEOPLEPORTAL_SMTP_PASSSMTP password
PEOPLEPORTAL_SMTP_DEFAULTFROMDefault From address when a request does not set one
PEOPLEPORTAL_EMAIL_CONREROUTEIf set, mail is intercepted instead of sent

PEOPLEPORTAL_SMTP_DEFAULTFROM is the From fallback, not PEOPLEPORTAL_SMTP_USER. Set PEOPLEPORTAL_EMAIL_CONREROUTE on any environment seeded with real member addresses, or onboarding mail reaches real people.

The dev stack sets SMTP to 127.0.0.1:1025, SECURE=false, with development credentials accepted by Mailpit. Messages stay in the local inbox at http://localhost:8025 without external delivery.

Photo check​

VariableNotes
PHOTO_CHECK_ENABLEDOff unless exactly true
PHOTO_CHECK_URLSidecar address. Default http://localhost:8001
PHOTO_CHECK_FAIL_CLOSEDtrue rejects uploads the service could not rule on

Leave PHOTO_CHECK_ENABLED off until the sidecar is actually running, otherwise every upload is waved through.

AWS​

VariableNotes
AWS_ACCESS_KEY_IDServer's IAM user
AWS_SECRET_ACCESS_KEY
AWS_REGIONDefault us-east-1
S3_BUCKET_NAMEResumes and avatars
AWS_ORG_ROOT_IDOrganization root, r-...
AWS_NONPROD_OU_IDOU new team accounts are moved into
AWS_SUSPENDED_OU_IDOU archived accounts move to
AWS_MANAGEMENT_ACCOUNT_IDPayer account, where budgets live
AWS_ADMIN_ROLE_NAMERole created inside each member account. Default AppDevNonProductionRole
AWS_DEFAULT_BUDGET_AMOUNTMonthly USD limit. Default 50
AWS_BILLING_ALERT_EMAILBudget alert recipient
AWS_DENY_ALL_SCP_IDDenyAll SCP, p-...
AWS_BUDGET_ACTION_ROLE_ARNRole AWS Budgets assumes to apply that SCP

The rest degrade. Without AWS_DENY_ALL_SCP_ID or AWS_BUDGET_ACTION_ROLE_ARN, budget enforcement logs a warning and skips while provisioning still succeeds. Without AWS_SUSPENDED_OU_ID, archiving throws.

AWS_ADMIN_ROLE_NAME both names the role created inside new accounts and names the role assumed for console links, so changing it breaks console access for accounts created under the old name.

Policy documents live in peopleportal/server/aws/.

The local dev environment includes no S3 object storage service. Resume/avatar uploads will fail until a local S3 option is added. Garage, SeaweedFS and versitygw are candidates for a follow-up; no replacement has been selected. The stack keeps S3 settings as placeholders: S3_BUCKET_NAME=people-portal, S3_ENDPOINT=http://s3.example.invalid, S3_FORCE_PATH_STYLE=true, and dummy AWS access/secret values. That placeholder endpoint is deliberately nonfunctional. The current server ignores endpoint/path-style settings and AWS_ENABLED=false. Dummy organization IDs satisfy startup checks only. AWS provisioning and uploads are unsupported locally. This stack changes no server S3/AWS behavior. verify.sh prints a NOTE for this known gap and verifies only the included backing services.

Node​

VariableNotes
NODE_TLS_REJECT_UNAUTHORIZED0 disables all TLS verification
NODE_EXTRA_CA_CERTSPath to an extra CA bundle

Setting NODE_TLS_REJECT_UNAUTHORIZED=0 in production is refused at startup. For internal or self-signed certificates use NODE_EXTRA_CA_CERTS, which trusts your CA without disabling verification globally.

Copyright © 2026 Atheesh Thirumalairajan