# Deployment Topology 🟒 **Decided 2026-06-13.** Four core domains, one API β€” plus a fifth surface `ops.` added 2026-06-23 (🟒 Vue 3 SPA; see below & decision log). ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ www.loveatfirstfight.com β”‚ β”‚ app.loveatfirstfight.com β”‚ β”‚admin.loveatfirstfight.comβ”‚ β”‚ ──────────────────────── β”‚ β”‚ ──────────────────────── β”‚ β”‚ ──────────────────────── β”‚ β”‚ Nuxt (Vue SSR) β”‚ β”‚ Vue 3 SPA β”‚ β”‚ Vue 3 SPA β”‚ β”‚ PUBLIC β€” SEO matters β”‚ β”‚ CUSTOMER / MEMBER area β”‚ β”‚ STAFF / ADMIN β”‚ β”‚ β€’ marketing pages β”‚ β”‚ β€’ login / dashboard β”‚ β”‚ β€’ CRM, orders, refunds β”‚ β”‚ β€’ funnels / quizzes β”‚ β”‚ β€’ my courses / lessons β”‚ β”‚ β€’ course authoring β”‚ β”‚ β€’ landing / opt-ins β”‚ β”‚ β€’ account / billing β”‚ β”‚ β€’ automations / reports β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό HTTPS Β· JSON Β· Sanctum tokens β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ api.loveatfirstfight.com β”‚ β”‚ (future) Flutter app β”‚ β”‚ ──────────────────────── β”‚ ◀───── β”‚ consumes the same API, β”‚ β”‚ Laravel β€” JSON API only β”‚ β”‚ no re-architecture β”‚ β”‚ /api/v1 Β· OpenAPI spec β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ MySQL/Postgresβ”‚ β”‚ Payment PSP β”‚ β”‚ Email / CRM β”‚ β”‚ (owned data) β”‚ β”‚ (Stripe/ β”‚ β”‚ (ActiveCampaign) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ PayPal) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Domains | Domain | Stack | Surface | Auth | SEO | |---|---|---|---|---| | `www.loveatfirstfight.com` | **Nuxt** (Vue SSR) | Public marketing & funnel | Mostly anonymous | **Yes** (SSR) | | `app.loveatfirstfight.com` | **Vue 3 SPA** | Customer / member | Sanctum token | No (behind login) | | `admin.loveatfirstfight.com` | **Vue 3 SPA** | Staff / admin | Sanctum token (staff role) | No | | `ops.loveatfirstfight.com` | **Vue 3 SPA** | Staff procedures / SOPs (Bruce's "place for all procedures") | Sanctum token (staff role) | No | | `api.loveatfirstfight.com` | **Laravel** | JSON API for all clients | issues/validates tokens | n/a | > The future **Flutter app** is another client of the *same* `api.` host β€” no new > backend, no Inertia to unwind. That's the whole point of API-first. > 🟒 **`ops.` added 2026-06-23 (Berner).** A staff-facing home for all of LAFF's > procedures/SOPs (a requirement from Bruce). **Built as its own Vue 3 SPA** on > `ops.` (same pattern as `app.`/`admin.`: behind login, no SEO, consumes the > same API) β€” rejected `wiki.` as too generic and rejected an off-the-shelf wiki > tool in favour of owning it. It's a fourth front-end origin for **CORS** and > belongs in `SANCTUM` staff-role scope. Content scope (which SOPs, authoring > flow) still being fleshed out. The ASCII diagram above still shows the original > four; redraw to include `ops.` as a fifth front-end. See decision log. ## What this topology forces us to get right ### 1. Auth across subdomains (Sanctum) - Use **Sanctum token-based auth** (Bearer tokens), not cookie/session SPA mode. Reason: tokens work identically for the two SPAs *and* the future Flutter app; no shared-cookie-domain gymnastics. - (If we ever wanted cookie SPA mode instead, all app domains must sit under the same registrable domain β€” they do β€” and `SANCTUM_STATEFUL_DOMAINS` + `SESSION_DOMAIN=.loveatfirstfight.com` would be set. **Not our chosen path**, but noted because it's a common fork.) ### 2. CORS (Laravel `config/cors.php`) `api.` must allow requests from the three front-end origins: ``` allowed_origins = [ 'https://www.loveatfirstfight.com', 'https://app.loveatfirstfight.com', 'https://admin.loveatfirstfight.com', 'https://ops.loveatfirstfight.com', ] ``` Plus the local dev equivalents (e.g. `http://localhost:3000`, `:5173`, `:5174`). ### 3. Nuxt does SSR β†’ server-to-server API calls The Nuxt site renders on a server, so it calls `api.` from the **server side** for public data (course catalog, marketing content). Plan a read path / public endpoints that don't require a user token (cacheable, rate-limited). ### 4. Environments Each domain gets `dev` / `staging` / `prod`. Suggested pattern: `api.staging.loveatfirstfight.com`, `app.staging.…`, etc. (or a separate staging registrable domain). Decide once; keep it consistent. **🟒 Dev environment (decided 2026-06-23):** runs in **Docker on Berner's PC** behind the existing **shared Traefik v3.6** (`D:\Development\traefik\`). Dev hosts are `dev.{api,app,admin,ops,www}.loveatfirstfight.com`. Each service joins the external `traefik-network` and routes via **docker labels** with `tls.certresolver=letsencrypt` (HTTP-01) β€” the same pattern as the other dev stacks on this box (SuperMatch, cartli, ebenware). **Requires public A records β†’ `156.155.180.202`** for ACME to validate (the public IP hairpins back to the PC). See the decision log entry "Dev environment = local Docker on existing shared Traefik". ### 5. TLS / DNS Four A/CNAME records + wildcard or per-host TLS (`*.loveatfirstfight.com` wildcard cert is simplest). The `.co.za` legacy domains redirect to these later. ## Repo shape (suggested) Either a monorepo or four repos: ``` laff-api/ Laravel β†’ api.loveatfirstfight.com laff-web/ Nuxt β†’ www.loveatfirstfight.com laff-app/ Vue SPA β†’ app.loveatfirstfight.com laff-admin/ Vue SPA β†’ admin.loveatfirstfight.com (laff-shared/ TS types / OpenAPI-generated client, shared by web+app+admin) ``` A `laff-shared` package generated from the OpenAPI spec gives all three JS clients **typed, always-in-sync** API calls. Open question: monorepo vs multi-repo.