# Every variable the application actually reads, so a rebuilt secrets file cannot silently drop one. # On the server this is /etc/threadcount/secrets.env; locally it is .env.local. # # The variables beginning NEXT_PUBLIC_ are compiled into the browser bundle at build time, not read # at runtime — set them before `next build`, and never put anything secret in one. # ---- required. instrumentation.ts refuses to start production without these ---- DATABASE_URL=postgresql://user:pass@localhost:5432/threadcount # Signs every session cookie. Generate one per environment (`openssl rand -base64 48`) and never # ship the placeholder — production refuses to start while it still says change-me. SESSION_SECRET=change-me # Cloudflare Turnstile. Protects sign-in, sign-up, password reset, the contact form and the update # list. Missing in production means either an unprotected front door or an authentication outage, # so the server refuses to start without both. TURNSTILE_SECRET= NEXT_PUBLIC_TURNSTILE_SITEKEY= # ---- transactional mail ---- # With these unset nothing is sent: a request is still raised, and the screens say plainly that # nobody was emailed rather than claiming otherwise. SMTP_HOST= SMTP_PORT=587 SMTP_USER= SMTP_PASS= SMTP_FROM="ThreadCount " # Where the website's contact form is delivered. CONTACT_TO= # ---- addresses and storage ---- # Absolute base URL, used for links in emails (approval links, password resets). NEXT_PUBLIC_SITE_URL=https://threadcount.tech # Where uploaded photographs are written. Must be on a volume that is backed up with the database. PHOTO_DIR=/var/lib/threadcount/photos # ---- Android app links ---- # The SHA-256 signing-certificate fingerprints served from /.well-known/assetlinks.json. Several may # be listed per app, comma-separated. ANDROID_APP_FINGERPRINTS is the fallback for both. ANDROID_APP_FINGERPRINTS_COUNTER= ANDROID_APP_FINGERPRINTS_STAFF= # ---- operations console (ops.threadcount.tech) ---- # Signs the operator session cookie (tc_ops). Its OWN secret, never SESSION_SECRET: that one also # derives the key encrypting every customer's TOTP secret, so sharing it would mean a compromised # operator credential forces a rotation that destroys every facility's second factor. Generate it # the same way (`openssl rand -base64 48`). Only the console reads it; the product starts without it. OPS_SESSION_SECRET= # The console reads customer data through restricted Postgres roles, never the app role: # ops_ro — SELECT on control-plane columns only (counts, dates, configuration). # ops_reveal — SELECT on a facility's id and three coordinator contact columns, nothing else. # Same host and database as DATABASE_URL, different user. Create the roles by hand as postgres; # the grants are in prisma/migrations (ops_ro_grants, ops_reveal_grants). OPS_DATABASE_URL= OPS_REVEAL_DATABASE_URL= # Single sign-on for FACILITIES (coordinators and, if a facility allows it, staff): the self-hosted # BoxyHQ Jackson broker at sso.threadcount.tech holds each facility's IdP metadata. With either # unset there is no SSO anywhere in the product; the key authenticates the management API only. JACKSON_URL= JACKSON_API_KEY= # Single sign-on for the console: ops.threadcount.tech sits behind a Cloudflare Access application # (Authentik + one-time-PIN). With BOTH set, the app verifies the Access assertion (RS256, fail # closed) and mints the operator session from the verified email; an email Access admits but the # console does not know falls back to the password door. With either unset there is no SSO. CF_ACCESS_TEAM_DOMAIN= CF_ACCESS_AUD= # Every contact reveal emails a notice here (who, which facility, why, until when — never the # contacts). Defaults to the revealing operator's own address. OPS_ALERT_TO= # ---- optional switches ---- # 1 hides the create-account form and refuses the signup endpoint. SIGNUPS_DISABLED= # 1 takes the public demo facility out of service. DEMO_DISABLED= # Bearer token the demo facility's scheduled reset presents. DEMO_RESET_TOKEN= # Local production-mode smoke tests ONLY: 1 makes Turnstile advisory so `next start` and the e2e # runners work on a box with no Cloudflare keys. NEVER set this in /etc/threadcount/secrets.env. TURNSTILE_OPTIONAL= # ---- optional: analytics, errors, update list ---- # All three default to ThreadCount's own self-hosted infrastructure; set them only to point # somewhere else. NEXT_PUBLIC_UMAMI_SITE_ID= NEXT_PUBLIC_UMAMI_APP_ID= NEXT_PUBLIC_GLITCHTIP_DSN= # Stamped on error reports so a fault can be tied to a deploy. NEXT_PUBLIC_RELEASE= LISTMONK_URL= LISTMONK_LIST_UUID= # ---- local development only ---- # 1 is required by the embedded `prisma dev` server, which cannot handle concurrent queries. # Never set it in production: it serialises every database call in the app. DB_POOL_MAX= # Plans. 1 forces plans live on this box whatever the console switch says: new sign-ups start on # Hosted Small (60 staff records) and Settings shows the Plan tab. Leave unset; flip it from the # console once the notice to existing rooms has run. # PLANS_LIVE=1 # ---- edition ---- # "community" = the self-hosted edition (Dockerfile / docker-compose.yml): every feature, no plans, # no staff ceiling, no demo, no operations console, no error reports or usage statistics sent # anywhere, and Turnstile optional. Leave unset on threadcount.tech. EDITION= # Only docker-compose.yml reads this: the password of the bundled Postgres. POSTGRES_PASSWORD=