All environment variables are set in the Coolify UI for deployed environments. Never committed to the repository.
API (apps/api)
Required — will not start without these
| Variable | Example | Notes |
|---|
DATABASE_URL | postgresql://user:pass@host:5432/pcmr_production | PostgreSQL connection string |
BETTER_AUTH_SECRET | (32+ random bytes, hex) | Session encryption. Must match web app. |
BETTER_AUTH_URL | https://pcmr.gr | Frontend public URL — not the API URL. Better Auth builds email links from this. |
FRONTEND_URL | https://pcmr.gr | Used for CORS and email links |
Auth & OAuth
| Variable | Notes |
|---|
COOKIE_DOMAIN | Empty string for local dev. .pcmr.gr for prod (leading dot = all subdomains). Staging: .staging.pcmr.gr |
GOOGLE_CLIENT_ID | Google OAuth app credentials |
GOOGLE_CLIENT_SECRET | Google OAuth app credentials |
Payment (Viva Smart Checkout)
| Variable | Demo value | Production value |
|---|
VIVA_BASE_URL | https://demo-api.vivapayments.com | https://api.vivapayments.com |
VIVA_AUTH_URL | https://demo-accounts.vivapayments.com | https://accounts.vivapayments.com |
VIVA_CLIENT_ID | From Viva demo account | From Viva production account |
VIVA_CLIENT_SECRET | From Viva demo account | From Viva production account |
VIVA_SOURCE_CODE | 4-digit code from Viva | Different code for production |
VIVA_WEBHOOK_KEY | From Viva webhook config | From Viva webhook config |
All 5 Viva vars are validated at startup — missing any one will crash the API.
File Storage (Hetzner Object Storage)
| Variable | Staging | Production |
|---|
STORAGE_ENDPOINT | https://nbg1.your-objectstorage.com | https://hel1.your-objectstorage.com |
STORAGE_BUCKET | mneme-staging | mneme |
STORAGE_REGION | nbg1 | hel1 |
STORAGE_ACCESS_KEY | Staging key | Production key |
STORAGE_SECRET_KEY | Staging secret | Production secret |
STORAGE_PRESIGN_EXPIRY | 3600 (default) | 3600 (default) |
Email (Zoho SMTP)
| Variable | Value | Notes |
|---|
SMTP_HOST | smtppro.zoho.eu | |
SMTP_PORT | 587 | STARTTLS |
SMTP_SECURE | false | Don't use SSL on connect for port 587 |
SMTP_USER | [email protected] | Full Zoho email address |
SMTP_PASS | (Zoho password) | Rotate if compromised |
EMAIL_FROM | PCMR.gr <[email protected]> | Display name + address |
STAFF_NOTIFY_EMAIL | [email protected] | Receives quote-accepted notifications |
WAITLIST_NOTIFY_EMAIL | [email protected] | Comma-separated; receives waitlist entries |
Observability
| Variable | Notes |
|---|
LOKI_URL | http://10.1.0.4:3100 (Olympus network) |
LOKI_HOST | Same as LOKI_URL |
GLITCHTIP_DSN | From GlitchTip project settings |
Optional / Defaults
| Variable | Default | Notes |
|---|
PORT | 3001 | NestJS listen port |
NODE_ENV | development | Set to production in deployed containers |
Web (apps/web)
Build-time (inlined by Next.js — must be set as Docker build args)
| Variable | Example | Notes |
|---|
NEXT_PUBLIC_API_URL | https://api.pcmr.gr | Browser-side API base URL. Do not use internal hostname here. |
NEXT_PUBLIC_GLITCHTIP_DSN | (from GlitchTip) | Client-side error tracking. Must be the public URL, not the internal Iris hostname. |
NEXT_PUBLIC_VIVA_CHECKOUT_URL | https://demo.vivapayments.com | Checkout redirect base. Defaults to demo if unset. |
Runtime (server-side only)
| Variable | Example | Notes |
|---|
API_URL | http://10.1.0.1:3001 | Internal NestJS URL for server-side fetches. Avoids public network / Cloudflare round-trip. |
BETTER_AUTH_SECRET | (same as API) | Must match API's BETTER_AUTH_SECRET exactly. |
BETTER_AUTH_URL | https://api.pcmr.gr | Used for server-side session validation (getServerSession()). Set to the API's public URL here (opposite of the API setting). |
GLITCHTIP_DSN | (from GlitchTip) | Server-side error tracking DSN |
Common Mistakes
BETTER_AUTH_SECRET mismatch
If the API and web have different BETTER_AUTH_SECRET values, session cookies signed by one cannot be validated by the other. Symptom: all users appear logged out, getSession returns null.
NEXT_PUBLIC_API_URL pointing to internal hostname
NEXT_PUBLIC_API_URL is embedded in the client-side JavaScript bundle. If set to http://10.1.0.1:3001, browser requests will fail because users can't reach the private WireGuard IP. Always use the public domain.
NEXT_PUBLIC_GLITCHTIP_DSN using internal hostname
Same issue — client-side code runs in the browser. Use the public GlitchTip URL (e.g., https://glitchtip.ctsolutions.gr/...), not the internal Iris address.
COOKIE_DOMAIN set to localhost
Browsers reject Domain=localhost. Leave COOKIE_DOMAIN empty for local development — the cookie will be set without a domain restriction and work on localhost.
BETTER_AUTH_URL confusion
The naming is confusing:
- API container: Set to the frontend URL (
https://pcmr.gr) — Better Auth uses it to build email verification links
- Web container: Set to the API URL (
https://api.pcmr.gr) — getServerSession() calls this URL
Wrong Viva base URL
VIVA_BASE_URL must be demo-api.vivapayments.com for demo (not demo.vivapayments.com). The checkout page URL is separate and used only for browser redirects.