UnionOps — stewarded by Ryan Morris.
If you host an instance, you are the data controller for data that instance stores (sessions, Officer Hub records, future database). Prefer Canadian hosting for confidential modules. Comms graphics tools remain on-device in the browser; the Officer Hub does not.
- Set a unique
AUTH_SECRET(openssl rand -base64 32) — never use the repo placeholders. Production refuses to start without it. - Set
AUTH_URLto your public HTTPS origin (no trailing slash). Behind CapRover or custom nginx, this must be the browser-facing host (e.g.https://unionops.org), not the internal app FQDN — otherwise locale redirects and Auth cookies advertise the proxy hostname to crawlers. - MFA is opt-in (
AUTH_MFA_ENABLED=true). Leave it off for demos/usability. For real casework: enable MFA and setAUTH_MFA_MODE=totp. Workshop hosts may useshared_code_insecureonly withAUTH_ALLOW_SHARED_MFA_IN_PROD=trueplus a uniqueAUTH_MFA_CODE. When MFA is enabled in production, unset mode or shared-code without break-glass fails closed. - Set your union’s default brand (optional but recommended for a white-label host):
- Edit
config/host-brand.jsonbefore build, or npm run brand:set -- --primary=#… --secondary=#… --local=… --sub="…", or- Pass
NEXT_PUBLIC_BRAND_PRIMARY/SECONDARY/ACCENTandNEXT_PUBLIC_DEFAULT_LOCAL_NUMBER/SUB_TEXTas container env (see.env.example).
- Edit
- Do not rely on demo accounts (
demo123) for real grievances or member files. On workshop/demo hosts, setNEXT_PUBLIC_DEMO_SITE=trueso the authenticated hub shows a Demo banner; turn that off for real tenant instances. - Confirm health:
GET /api/health→{"status":"ok",...}(includesversion,commit,backends,emailEnabled,cronConfigured) ornpm run health:check. - CSP and related security headers are set in
next.config.ts(apply on CapRover/Docker/Vercel alike). - Read the two-tier privacy model in the site Privacy page and
docs/COMPLIANCE.md.
Containers publish to GitHub Container Registry from docker/Dockerfile.
Main tip (after successful CI on main — see .github/workflows/ci.yml):
ghcr.io/hackmods/union-communications:main
ghcr.io/hackmods/union-communications:sha-<short>
Tagged releases (v* — see .github/workflows/release.yml):
ghcr.io/hackmods/union-communications:v0.1.0
ghcr.io/hackmods/union-communications:latest
:latest is only updated on version tags, not on every main push.
Pull and run:
docker pull ghcr.io/hackmods/union-communications:v0.1.0
docker run --rm -p 3000:3000 \
-e AUTH_SECRET="$(openssl rand -base64 32)" \
-e AUTH_URL="https://your.domain.example" \
ghcr.io/hackmods/union-communications:v0.1.0See commented image: example in docker/docker-compose.yml.
cd docker
docker compose up --buildCompose requires AUTH_SECRET in the environment (or docker/.env). MFA stays off by default (AUTH_MFA_ENABLED=false); set it true plus mode/code when testing second-factor flows.
This repo includes captain-definition pointing at ./docker/Dockerfile.
Durable Postgres on CapRover: step-by-step walkthrough in CAPROVER_POSTGRES.md (two-app setup, env template, bootstrap seed, verify). Paste-ready env: docker/.env.production.example.
Prefer GHCR image pull on small droplets (ghcr.io/hackmods/union-communications:main) over CapRover git rebuilds — large COPY --from / node_modules layers can fail with BuildKit unknown parent image ID. Lessons: session-knowledge-2026-08-25-caprover-buildkit.md, .cursor/rules/caprover-docker.mdc. CI publishes images in the docker-image job (independent of E2E). Optional secrets: CAPROVER_SERVER, CAPROVER_PASSWORD, CAPROVER_APP.
- Create an app; set Container HTTP Port to 3000 (not 80). A wrong port yields CapRover NGINX 502 even when logs say Ready.
- App Configs (minimum):
| Variable | Example |
|---|---|
AUTH_SECRET |
output of openssl rand -base64 32 |
AUTH_URL |
Public HTTPS origin only (no trailing slash) — e.g. https://unionops.org. Never the CapRover/internal FQDN (*.behind7proxies.com); wrong value leaks internal hosts into locale redirects, Auth.js callback-url cookies, and GSC “Page with redirect” noise. |
AUTH_ALLOW_DEMO_USERS |
true on the public demo host so president.243@unionops.test / demo123 work. The image defaults this to match NEXT_PUBLIC_DEMO_SITE. Omit/false for live casework. |
AUTH_MFA_ENABLED |
true for casework; omit/false for demos (default) |
AUTH_MFA_MODE |
totp when MFA enabled; workshops: shared_code + break-glass |
AUTH_ALLOW_SHARED_MFA_IN_PROD |
true only for workshop/demo hosts using shared code |
AUTH_MFA_CODE |
6-digit code when using shared_code mode |
Optional transactional email (invites / officer reminders / RSVP confirm — ADR-016):
| Variable | Example |
|---|---|
EMAIL_ENABLED |
true (required to send; otherwise APIs return not_configured) |
NEXT_PUBLIC_EMAIL_ENABLED |
true (Hub Invites “Send email” button; bake at build time) |
EMAIL_FROM |
UnionOps <noreply@your-verified-domain> (must match a Mailgun-verified domain) |
MAILGUN_API_KEY |
Mailgun Private API key (Sending → API Security) — required on DigitalOcean |
MAILGUN_DOMAIN |
Verified sending domain (e.g. unionops.org or mg.unionops.org) |
MAILGUN_API_REGION |
us (default) or eu |
DigitalOcean CapRover: outbound SMTP on 25 / 465 / 587 is blocked (CONN ETIMEDOUT). Do not rely on SMTP_* there — set MAILGUN_API_KEY + MAILGUN_DOMAIN so UnionOps sends over HTTPS (api.mailgun.net). Confirm with GET /api/auth/email-status/ → smtp.preferredTransport: "mailgun_api".
Optional SMTP (non-DO hosts, or Mailgun port 2525 if your network allows it):
| Variable | Example |
|---|---|
SMTP_HOST |
smtp.mailgun.org (EU: smtp.eu.mailgun.org) |
SMTP_PORT |
2525 preferred when 465/587 are blocked; else 587 |
SMTP_USER |
Mailgun SMTP login (often postmaster@mg.your-domain) |
SMTP_PASS |
Mailgun SMTP password from Domain → SMTP credentials |
When MAILGUN_API_KEY is set it takes priority over SMTP. On SMTP CONN timeout to 465/587 the app retries port 2525 once, then surfaces a DigitalOcean hint.
Optional error sinks (ADR-006 — ops only, not product analytics; defaults off). Full matrix: HOSTED_SECURITY.md.
| Variable | Example |
|---|---|
SENTRY_ENABLED |
true (server/edge; needs DSN) |
NEXT_PUBLIC_SENTRY_DSN |
Sentry client key — bake at build for browser errors |
SENTRY_DSN |
Optional runtime server DSN (CapRover App Configs OK) |
ERROR_LOG_FILE_ENABLED |
true |
ERROR_LOG_FILE_PATH |
/data/logs/unionops-errors.jsonl (mount a Persistent Directory) |
ERROR_LOG_FILE_MAX_BYTES |
optional rotate threshold (default 10 MiB) |
ERROR_LOG_FILE_KEEP |
optional rotated file count (default 3) |
After deploy, curl -sL https://<host>/api/health/ → check observability (and watch for *Misconfigured / sentryClientServerMismatch).
Optional brand defaults — bake into the image at build time (NEXT_PUBLIC_* is inlined by Next.js). Prefer editing config/host-brand.json (or npm run brand:set) before docker build when you want a white-label host without env sprawl:
| Variable | Example |
|---|---|
NEXT_PUBLIC_BRAND_PRIMARY |
#CE1126 |
NEXT_PUBLIC_BRAND_SECONDARY |
#FFFFFF |
NEXT_PUBLIC_BRAND_ACCENT |
#9B0D1C |
NEXT_PUBLIC_DEFAULT_LOCAL_NUMBER |
79 |
NEXT_PUBLIC_DEFAULT_SUB_TEXT |
Hospital Workers |
NEXT_PUBLIC_OFFICER_HUB_PUBLIC |
true (Docker soft-launch default) |
NEXT_PUBLIC_DEMO_SITE |
true on demo hosts; false for live tenants. Bake at build time (login hint). The runner image also sets AUTH_ALLOW_DEMO_USERS to the same value so production authorize() matches the hint. |
- Deploy via CapRover git push / webhook, or pull the GHCR tag if your CapRover setup uses a registry image.
- Health check:
GET /api/health. - Public
unionops.orghost: point the installable origin at apexhttps://unionops.org. SetAUTH_URL=https://unionops.org(same origin). Untilwwwserves this app (or 301s to apex) with a trusted certificate, leavewwwoff the PWA service-worker allowlist (src/lib/pwa/hosts.ts). After deploy, spot-checkcurl -sI https://unionops.org/examples/—Locationmust stay onunionops.org, not the CapRover hostname.
CI on main can POST CAPROVER_WEBHOOK_URL (GitHub Actions secret) after tests pass.
/app/hybrid encrypts export/import in the browser. Passphrases never leave the client. Hybrid is a backup preference today — it does not replace configuring a secure hosted store for live multi-user grievance data.
- Local setup:
SETUP.md - Postgres durability:
POSTGRES_OPS.md - CapRover + Postgres flip:
CAPROVER_POSTGRES.md - Security reporting:
SECURITY.md
For the CT 115 lab host (192.168.0.115:3000):
- On your workstation:
npm run package:sandbox→unionops-src.tar.gz(git archive, nonode_modules). - Copy the tarball to the container (e.g.
/tmp/unionops-src.tar.gz). - Overlay into
/root/unionops-src(tar -xzf— do not wipe the tree withrm -rf). - Rebuild:
docker build -f docker/Dockerfile -t unionops:local .withBUILD_COMMIT_SHAset to the deployed commit. - Restart the
unionopscontainer preserving env fromdocker inspect(never logAUTH_SECRET). - Verify:
npm run health:checkandnpm run test:smoke:sandboxfrom your workstation.