Skip to main content

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.example wins — update this page to match.

API (apps/api)

Required — will not start without these

VariableExampleNotes
DATABASE_URLpostgresql://user:pass@host:5432/pcmr_productionPostgreSQL connection string
BETTER_AUTH_SECRET(32+ random bytes, hex)Session encryption. Must match web app.
BETTER_AUTH_URLhttps://pcmr.grFrontend public URL — not the API URL. Better Auth builds email links from this.
FRONTEND_URLhttps://pcmr.grUsed for CORS and email links

The Viva vars (below) are also validated at startup — missing any one crashes the API on boot.

Auth & OAuth

VariableNotes
COOKIE_DOMAINEmpty string for local dev. .pcmr.gr for prod (leading dot = all subdomains). Staging: .staging.pcmr.gr
GOOGLE_CLIENT_IDGoogle OAuth app credentials
GOOGLE_CLIENT_SECRETGoogle OAuth app credentials

Payment (Viva Smart Checkout)

Values below match .env.example (authoritative). All are validated at startup.

VariableDemo valueProduction value
VIVA_BASE_URLhttps://demo.vivapayments.comhttps://www.vivapayments.com
VIVA_AUTH_URLhttps://demo-accounts.vivapayments.comhttps://accounts.vivapayments.com
VIVA_CLIENT_IDFrom Viva demo accountFrom Viva production account
VIVA_CLIENT_SECRETFrom Viva demo accountFrom Viva production account
VIVA_SOURCE_CODE4-digit code from VivaDifferent code for production
VIVA_WEBHOOK_KEYFrom Viva webhook configFrom 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)

VariableStagingProduction
STORAGE_ENDPOINThttps://nbg1.your-objectstorage.comhttps://hel1.your-objectstorage.com
STORAGE_BUCKETmneme-stagingmneme
STORAGE_REGIONnbg1hel1
STORAGE_ACCESS_KEYStaging keyProduction key
STORAGE_SECRET_KEYStaging secretProduction secret
STORAGE_PRESIGN_EXPIRY3600 (default)3600 (default)

Email (Zoho ZeptoMail SMTP)

Transactional email goes through Zoho ZeptoMailnot Zoho Mail (smtppro). Migrated from Zoho SMTP; the old SMTP_PASS / EMAIL_FROM / STAFF_NOTIFY_EMAIL / WAITLIST_NOTIFY_EMAIL names are gone.

VariableValue / ExampleNotes
SMTP_HOSTsmtp.zeptomail.eu
SMTP_PORT587STARTTLS
SMTP_SECUREfalseDon't use SSL on connect for port 587
SMTP_USERemailapikeyLiteral 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):

VariableExampleNotes
MAIL_DEFAULT_FROMPCMR.gr <[email protected]>Fallback for any category
MAIL_FROM_AUTHPCMR.gr <[email protected]>
MAIL_FROM_ORDERSPCMR.gr <[email protected]>
MAIL_FROM_WARRANTYPCMR.gr <[email protected]>
MAIL_FROM_WAITLISTPCMR.gr <[email protected]>
MAIL_REPLY_TOPCMR.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).

VariableExample
LISTMONK_URLhttps://newsletter-admin.pcmr.gr
LISTMONK_API_USERlistmonk-api-user
LISTMONK_API_TOKEN(from Listmonk)
LISTMONK_NEWSLETTER_LIST_ID1 (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.

VariableNotes
GSIS_USERNAMEAADE credentials
GSIS_PASSWORDAADE credentials
GSIS_CALLER_AFMThe company's own ΑΦΜ (required by AADE)

Observability + telemetry

VariableNotes
LOKI_URL / LOKI_HOSThttp://10.1.0.4:3100 (Olympus network). The pino-loki transport reads LOKI_HOST.
GLITCHTIP_DSNFrom GlitchTip project settings (use the public URL)
UPTIME_KUMA_URLhttp://10.1.0.4:3001 — live uptime/response telemetry for /status
UPTIME_KUMA_SLUGStatus-page slug (default main)
LOKI_TELEMETRY_ENABLEDtrue to enable the Loki p50 response-time query (off by default)

Optional / Defaults

VariableDefaultNotes
PORT3001NestJS listen port
NODE_ENVdevelopmentSet to production in deployed containers

Web (apps/web)

Build-time (inlined by Next.js — must be set as Docker build args)

VariableExampleNotes
NEXT_PUBLIC_API_URLhttps://api.pcmr.grBrowser-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_URLhttps://demo.vivapayments.comCheckout redirect base. Defaults to demo if unset.

Runtime (server-side only)

VariableExampleNotes
API_URLhttp://10.1.0.2:3001Internal 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_URLhttps://api.pcmr.grUsed 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.

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.