Environment Variables
All environment variables are set in the Coolify UI for deployed environments. Never committed to the repository.
Source of truth:
apps/api/.env.example. If this page and that file ever disagree,.env.examplewins — update this page to match.
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 |
The Viva vars (below) are also validated at startup — missing any one crashes the API on boot.
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)
Values below match .env.example (authoritative). All are validated at startup.
| Variable | Demo value | Production value |
|---|---|---|
VIVA_BASE_URL | https://demo.vivapayments.com | https://www.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 |
VIVA_WEBHOOK_KEY is required for webhook signature verification. The
GET /payments/webhook key-echo endpoint is gated behind
VIVA_WEBHOOK_VERIFICATION_ENABLED (leave unset/false in prod; flip on only
during a Viva re-verification window, then off — and rotate the key).
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 ZeptoMail SMTP)
Transactional email goes through Zoho ZeptoMail — not Zoho Mail
(smtppro). Migrated from Zoho SMTP; the old SMTP_PASS / EMAIL_FROM /
STAFF_NOTIFY_EMAIL / WAITLIST_NOTIFY_EMAIL names are gone.
| Variable | Value / Example | Notes |
|---|---|---|
SMTP_HOST | smtp.zeptomail.eu | |
SMTP_PORT | 587 | STARTTLS |
SMTP_SECURE | false | Don't use SSL on connect for port 587 |
SMTP_USER | emailapikey | Literal string — ZeptoMail's fixed SMTP username |
SMTP_PASSWORD | (ZeptoMail Send token) | The ZeptoMail API/Send token. Rotate if compromised. (was SMTP_PASS) |
ZEPTOMAIL_WEBHOOK_SECRET | (ZeptoMail → Webhooks) | Bounce-webhook signature secret. Falls back to BETTER_AUTH_SECRET (warn) if unset. URL: https://api.pcmr.gr/api/mail/bounce-webhook |
From addresses (per category; fall back to MAIL_DEFAULT_FROM):
| Variable | Example | Notes |
|---|---|---|
MAIL_DEFAULT_FROM | PCMR.gr <[email protected]> | Fallback for any category |
MAIL_FROM_AUTH | PCMR.gr <[email protected]> | |
MAIL_FROM_ORDERS | PCMR.gr <[email protected]> | |
MAIL_FROM_WARRANTY | PCMR.gr <[email protected]> | |
MAIL_FROM_WAITLIST | PCMR.gr <[email protected]> | |
MAIL_REPLY_TO | PCMR.gr <[email protected]> | Reply-To on every outbound email |
ADMIN_NOTIFICATION_EMAIL | [email protected] | Waitlist / newsletter / allowlist notifications |
STAFF_NOTIFICATIONS_EMAIL | [email protected] | Inbound quote-request notices; falls back to ADMIN_NOTIFICATION_EMAIL |
Newsletter (Listmonk — self-hosted on Iris)
Graceful no-op when any var is missing (e.g. local dev).
| Variable | Example |
|---|---|
LISTMONK_URL | https://newsletter-admin.pcmr.gr |
LISTMONK_API_USER | listmonk-api-user |
LISTMONK_API_TOKEN | (from Listmonk) |
LISTMONK_NEWSLETTER_LIST_ID | 1 (numeric list id) |
LISTMONK_WEBHOOK_SECRET | (shared secret for webhook verify) |
GSIS ΑΦΜ lookup (Greek tax authority SOAP service)
Leave blank in local dev — the endpoint returns 501 Not Implemented when unset.
| Variable | Notes |
|---|---|
GSIS_USERNAME | AADE credentials |
GSIS_PASSWORD | AADE credentials |
GSIS_CALLER_AFM | The company's own ΑΦΜ (required by AADE) |
Observability + telemetry
| Variable | Notes |
|---|---|
LOKI_URL / LOKI_HOST | http://10.1.0.4:3100 (Olympus network). The pino-loki transport reads LOKI_HOST. |
GLITCHTIP_DSN | From GlitchTip project settings (use the public URL) |
UPTIME_KUMA_URL | http://10.1.0.4:3001 — live uptime/response telemetry for /status |
UPTIME_KUMA_SLUG | Status-page slug (default main) |
LOKI_TELEMETRY_ENABLED | true to enable the Loki p50 response-time query (off by default) |
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.2: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.2:3001, browser requests will fail because users can't reach the
private Hetzner network (Olympus) 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
Email: ZeptoMail, not Zoho Mail
SMTP_HOST is smtp.zeptomail.eu and SMTP_USER is the literal emailapikey
(not an address). The password var is SMTP_PASSWORD (not SMTP_PASS). Using the
old Zoho smtppro.zoho.eu values means every transactional email silently fails.