1. Architecture chosen
Manager pushes environment variables into every business's .env, rather than businesses fetching config at runtime. Manager has no database of its own today (confirmed via code — no Prisma, no ORM dependency anywhere in manager/); every piece of its state is either an env var or a flat file under STACKS_DIR. A "runtime shared-config source" design would make every single email send (including OTP login) depend on Manager being reachable — a new failure mode this app has never had, for no real benefit. Instead:
Storage: one JSON file, stacks/manager/global-settings.json (gitignored), shaped for future sections (SMS, storage, backups, branding...) to slot in as sibling keys.
Secret at rest: the SMTP password is AES-256-GCM encrypted with a new MANAGER_SETTINGS_ENCRYPTION_KEY, never returned to the browser.
Rollout: saving complete settings automatically pushes the 7 EMAIL_* keys into every business's .env via the exact same surgical upsertEnvValues helper updateBusiness() already uses for name/domain edits, then force-recreates each container (composeUp(dir, {forceRecreate:true})) — the identical mechanism that already applies domain changes today.
New businesses get the current global settings baked into their .env at provisioning time — no manual step, ever.
2. Files changed
New (Manager): global-settings.ts (storage+encryption), email-rollout.ts (push-to-all-businesses), test-email.ts (standalone nodemailer test-send), dashboard/settings/ (page.tsx, actions.ts, email-settings-form.tsx).
Modified (Manager): compose-template.ts (optional email block in generated .env), provisioning.ts (reads global settings at creation), dashboard/layout.tsx (Settings nav link), package.json/package-lock.json (added nodemailer).
Modified (main app): brevo-email-provider.ts (added encryption mode + replyTo, backward-compatible), its test file (8 new tests).
Modified (docs): root .gitignore, .env.example, .env.business.example, manager/.env.example.
3. New volumes/databases/config files
None — no new Docker volume, no database. One new gitignored file, stacks/manager/global-settings.json, living inside the STACKS_DIR bind-mount Manager already has.
4. New environment variables
MANAGER_SETTINGS_ENCRYPTION_KEY (Manager only, required to use Settings) — 32-byte hex, generate with openssl rand -hex 32.
EMAIL_SMTP_ENCRYPTION and EMAIL_REPLY_TO (main app, both optional) — unset EMAIL_SMTP_ENCRYPTION preserves today's exact port-based inference, so existing deployments are unaffected.
Reply-to is now business-specific with global fallback,
5. How settings are persisted
stacks/manager/global-settings.json, written by saveEmailSettings(). The password is encrypted before writing; every other field is plain text (host, port, username, from-address, from-name, reply-to, encryption mode, last-test result) since none of those are secrets.
6. How business containers receive global settings
Via their own .env file (Docker Compose's env_file:, unchanged mechanism) — Manager writes the 7 EMAIL_* keys into it and recreates the container. Nothing in the business app itself needed a new runtime dependency; getEmailProvider() still just reads process.env once at process start, exactly as before.
7. What happens to existing businesses when settings change
Saving complete settings (host+username+password+from-address all present) automatically loops every business, updates its .env, and force-recreates its container in one action — the UI reports "applied to N of M businesses" with per-business failure reasons. A separate "Reapply to all businesses" button retries this without re-entering anything, for recovering from a business that was down during the last save. Saving incomplete settings persists them as a draft and touches no business (verified live — this is what stops a business from being flipped into a broken "brevo with blank credentials" state).
8. Manual deployment/update steps on the VPS
Generate a key: openssl rand -hex 32, add it as MANAGER_SETTINGS_ENCRYPTION_KEY to stacks/manager/.env.
Rebuild/redeploy the Manager image and the main app image (both changed) as usual.
Open Manager → Settings, fill in real SMTP details, send a test email, then Save — this rolls out to every existing business automatically. Nothing else needs to run by hand.
9. Security considerations
Password never logged, never returned to the browser after save (only a hasPassword boolean) — confirmed live: the form re-shows a masked placeholder, not the real value.
Test-email errors are categorized (auth/connection/timeout) before display — the raw SMTP error is never forwarded.
Encrypted at rest with AES-256-GCM; losing/rotating the key makes the stored password undecryptable (documented in manager/.env.example).
Settings routes sit under manager/src/proxy.ts's existing auth gate — no new auth surface.
Validated before rollout: malformed addresses/ports are rejected before Save, and rollout only fires when configuration is actually complete.
On JSON vs. database for Manager: JSON is the right call here. Reasoning:
Manager has zero DB dependencies today, deliberately — its own package.json description says an earlier attempt at giving it business-data access "repeatedly broke." Introducing SQLite/Prisma just for one settings blob breaks that design principle for not much gain.
The access pattern is a poor fit for a DB anyway: read on page-load/provisioning/rollout, written by a human clicking Save occasionally. No queries, no joins, no need for transactions beyond "read-modify-write one small file."
Reconsider if: multiple admins need to edit concurrently with conflict detection, you want an audit history of who-changed-what-when beyond "last test result," or the settings graph grows relational (e.g., per-business overrides tied by foreign key to businesses) rather than a flat global blob. None of that applies yet.
Key additions:
* Separate platform Manager stack: [stacks/manager/compose.yaml]
* Dedicated manager authentication with signed sessions.
* Business overview dashboard.
* Add Business workflow with slug suggestions and deployment progress.
* Business detail page with start, stop, restart, update, logs, Dockge, and conservative removal actions.
* Explicit management API endpoints under [src/app/api/manager]
* Shared provisioning service layer in [provisioning.service.ts]
* CLI and UI now use the same [create-business.sh] provisioning logic.
* Registry metadata, database status, container health, and media usage reporting.
* Manager-only entrypoint mode that does not require a business database.
* Updated deployment documentation with Manager setup and Caddy routing.
Validation passed:
* Infra, Caddy, Manager, and generated business Compose configurations.
* Shell syntax checks.
* Registry generation with registry image tags.
* git diff --check.
Seed data extended with a second barber (different hours/services), a beard-trim service + category, breaks, time off, a guest customer, and appointments in all three key states — verified against a live throwaway Postgres.
14 new integration tests (38 total with Phase 3's) covering guest/authenticated booking, any-barber assignment, slot-conflict handling, ownership enforcement, guest-data isolation, and book-again repricing.
Playwright E2E suite for the 6 required customer journeys, with its own global-setup.ts that migrates and seeds a database automatically.
Full verification pass, which caught and fixed three real bugs along the way (not just test-workarounds):
A date off-by-one bug in the calendar picker that made real weekdays show as closed for anyone in a UTC+ timezone (including the business's own).
A dialog that could vanish mid-confirmation on cancellation, before the customer could read the late-cancellation notice.
A silent test-cleanup leak in the Phase 3 fixtures that had been leaking stray businesses since Phase 3.
Plus a baseline desktop-responsive fix (centered max-w-2xl columns) across Home/Appointments/Settings/booking flow, since desktop had been effectively unstyled.
Final state: 90 unit tests, 38 integration tests, and all 6 E2E journeys passing together on a fresh database; clean tsc, eslint, and next build. Full writeup with routes, security model, guest-matching rules, known limitations, and what to check before Phase 5 is in docs/phase-4-summary.md.
Base app architecture:
Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.