# One business's config on the shared multi-business VPS setup — see # stacks/infra/compose.yaml for the one-time setup this depends on # (creating this business's database on the shared MySQL server) and # stacks//compose.yaml for how this file is used. Copy this to # `stacks//.env` (kept out of git, see .gitignore) per business and # fill in real values. Nothing here is a real credential. Normally the # management UI (manager/) generates this file for you — this template is # for the by-hand setup path. # # The app starts and runs a complete first-install flow with ONLY the # "Required for basic startup" section below filled in — every other # integration (email, push, Google sign-in, Google Calendar) shows a # disabled state rather than crashing when its variables are left blank. # ── Required for basic startup ───────────────────────────────────────── # Manager-only bookkeeping (shown on the dashboard/edit screen, editable # there) — the app itself never reads this; its own business name lives # in the database, set via /install. BUSINESS_NAME="Example Business" # The public domain this business is served on — must match the .caddy # site file's own domain (see stacks/infra/sites/.gitkeep) and that # domain's real DNS (pointed at the VPS) before Caddy can issue a # certificate for it. DOMAIN="example-business.example.com" # The app reaches MySQL via the shared infra's service hostname # `next-chair-mysql`, never `localhost` — see stacks/infra/compose.yaml. # DATABASE_NAME/USER/PASSWORD must match exactly what you created for # this business on the shared server (see stacks/infra/compose.yaml's # "CREATE DATABASE ..." step) — this file doesn't create them, it only # tells the app what to connect as. DATABASE_HOST="next-chair-mysql" DATABASE_PORT="3306" DATABASE_NAME="example_business" DATABASE_USER="example_business" DATABASE_PASSWORD="replace-me" # Generate with: npx auth secret AUTH_SECRET="replace-me-with-a-32-byte-random-string" # Auth.js needs to trust the host it's served from when not on localhost:3000. AUTH_TRUST_HOST="true" # Explicit, not just inferred from AUTH_TRUST_HOST + request headers — # header-based detection (Host/X-Forwarded-Host) behaves inconsistently # across reverse-proxy setups, and a wrong guess sends every post-sign-in # redirect to the wrong origin. Must match DOMAIN above exactly. AUTH_URL="https://example-business.example.com" NEXT_PUBLIC_APP_URL="https://example-business.example.com" # One-time setup token the installation wizard (/install) requires before # it will create the first business/administrator — generate with: # openssl rand -hex 32 INSTALLATION_TOKEN="replace-me" # Shared secret an external scheduler presents to # /api/cron/process-notifications — generate with: openssl rand -hex 32 CRON_SECRET="replace-me" # ── Super Administrator (optional — see scripts/ensure-super-admin.ts) ── # Platform-level account with every Owner permission for THIS business. # Not run automatically by this compose file (that's a Docker-entrypoint- # only behaviour, see docker/entrypoint.sh) — run it manually, once: # docker exec -it next-chair-example-business npx tsx scripts/ensure-super-admin.ts # Leave SUPER_ADMIN_EMAIL blank to skip entirely and use the /install # wizard's own administrator account instead. SUPER_ADMIN_EMAIL="" SUPER_ADMIN_PASSWORD="" SUPER_ADMIN_FIRST_NAME="" SUPER_ADMIN_LAST_NAME="" # ── Google sign-in (optional — "Continue with Google" on Sign In) ───── AUTH_GOOGLE_ID="" AUTH_GOOGLE_SECRET="" # ── Google Calendar sync (optional — staff-level, see /staff/settings/calendar) ─ # From https://console.cloud.google.com/apis/credentials — a separate # OAuth client from AUTH_GOOGLE_ID/SECRET above (different scopes). GOOGLE_CALENDAR_CLIENT_ID="" GOOGLE_CALENDAR_CLIENT_SECRET="" GOOGLE_CALENDAR_REDIRECT_URI="https://example-business.example.com/api/staff/calendar/google/callback" # Encrypts stored refresh tokens at rest — generate with: openssl rand -hex 32 CALENDAR_TOKEN_ENCRYPTION_KEY="" # ── Media Manager (optional — see src/lib/media/media-storage.ts) ────── # Max upload size in bytes for the Media Manager. Defaults to 5,000,000 # (5MB) if unset. MEDIA_MAX_UPLOAD_BYTES="5000000" # ── Google Reviews (optional — see /owner/settings/reviews) ──────────── # From https://console.cloud.google.com/apis/credentials — needs "Places # API (New)" enabled on that project (a different product from the older # "Places API" — enabling the wrong one returns REQUEST_DENIED). Leave # blank to keep review sync disabled entirely (Home shows clearly-labelled # demo reviews instead). GOOGLE_PLACES_API_KEY="" # ── Email (optional — defaults to logging to the console instead of sending) ─ # Managed centrally by the Manager's Settings > Email page and pushed # into every business's .env automatically — see manager/src/lib/ # global-settings.ts. There's normally no reason to hand-edit these for # one business only; this by-hand template still needs them set if # you're not using the Manager. EMAIL_PROVIDER="console" EMAIL_SMTP_HOST="smtp-relay.brevo.com" EMAIL_SMTP_PORT="587" EMAIL_SMTP_USERNAME="" EMAIL_SMTP_PASSWORD="" EMAIL_SMTP_ENCRYPTION="starttls" EMAIL_FROM_ADDRESS="" EMAIL_FROM_NAME="" EMAIL_REPLY_TO="" # ── Web Push (optional — defaults to logging to the console instead of sending) ─ # Managed centrally by the Manager's Settings > Push Notifications page and # pushed into every business's .env automatically — see manager/src/lib/ # global-settings.ts. There's normally no reason to hand-edit these for # one business only; this by-hand template still needs them set if you're # not using the Manager. No NEXT_PUBLIC_VAPID_PUBLIC_KEY here — the app # serves its public key at runtime via GET /api/push/vapid-public-key # instead (see that route's own header for why). PUSH_PROVIDER="console" VAPID_PUBLIC_KEY="" VAPID_PRIVATE_KEY="" VAPID_SUBJECT="mailto:owner@example.com"