# Proposed Architecture > Status: ๐ŸŸก proposal for discussion. Decisions tracked in > [../00-index/decision-log.md](../00-index/decision-log.md). ## Guiding principles 1. **Don't break the money.** ~$7.9M flows through checkout; migration must be reversible and staged. 2. **Own the core, rent the commodity.** Build LMS/commerce/automation in-house; it's fine to keep renting a PSP (Stripe) and maybe email (ActiveCampaign). 3. **One data model.** A single users table, one catalog, one order ledger โ€” the thing the two WordPress sites never had. 4. **Two surfaces, one backend.** Customers and staff are different worlds. --- ## Recommended stack | Layer | Choice | Why | |---|---|---| | Backend | **Laravel (PHP 8.3+)** | Your preference; auth, queues, jobs, mail, Eloquent, scheduler all built-in โ€” perfect for content+commerce+automation. | | Database | **PostgreSQL** | Chosen for stricter types, JSON, partitioning, analytical SQL. Functional + audit as **two databases on one server** (split-aware โ€” promote audit to its own server later on a trigger); behavioral analytics in a decoupled pipeline. Full detail: [data-stores-and-logging.md](data-stores-and-logging.md). | | Analytics | **In-house event pipeline** (collector โ†’ queue โ†’ store) | High-volume behavioral/A-B tracking, captured **async** so it never slows the request path. Postgres now โ†’ ClickHouse later. | | **API** | **Laravel as a pure JSON API** (Sanctum auth) | The single contract every client consumes โ€” both Vue SPAs today, a Flutter app tomorrow. Decoupled from day 1. | | Customer front end | **Vue 3 SPA** (own app, talks to the API) | Optimized for members; ships no admin code. | | Staff/admin front end | **Vue 3 SPA** (separate app, talks to the API) | Order management, CRM, course authoring; separate security surface & release cadence. | | Background work | **Laravel Queues + Horizon** (Redis) | Automation jobs, email sends, webhook processing, drip scheduling. | | Payments | **Stripe** (+ keep PayPal) | Direct integration replaces ThriveCart over time. | | Email/CRM | Keep **ActiveCampaign** initially | Don't rebuild marketing automation on day 1. | | Media/video | CDN (Bunny/S3 + signed URLs) | Locate current host first (open question). | --- ## Decided shape: API + two Vue SPAs (๐ŸŸข 2026-06-12) ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Customer Vue SPA โ”‚ โ”‚ Admin Vue SPA โ”‚ โ”‚ Flutter app โ”‚ โ”‚ (members/courses)โ”‚ โ”‚ (staff/CRM/ops) โ”‚ โ”‚ (future โ€” same โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ API, no rework) โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ HTTPS ยท JSON ยท token auth (Sanctum) โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ LARAVEL JSON API (only) โ”‚ โ”‚ versioned: /api/v1/... โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` **Why this over Inertia/Filament:** Berner wants a Flutter app on the table later. A pure JSON API is the **single contract** every client consumes โ€” build the business logic once, and the mobile app reuses the exact same endpoints with zero re-architecture. Inertia would couple the web app to Laravel's rendering and force us to bolt on a second API for mobile down the line; Filament admin screens wouldn't be reusable by Flutter at all. **Honest cost of this choice (so it's not a surprise):** - We build **auth ourselves** (Laravel Sanctum โ€” token-based, works for SPA *and* mobile) instead of getting session auth for free. - We **version and document the API** (`/api/v1`, OpenAPI spec) from the start. - We hand-build the **admin SPA** instead of getting Filament's CRUD for free โ€” more screens to write. Mitigation: a Vue admin component library (PrimeVue / Vuetify / shadcn-vue) to avoid building tables/forms from scratch. - **SEO:** public marketing/funnel pages need crawlable HTML. A pure SPA isn't great for that โ†’ see note below. ### The public marketing/funnel site โ†’ Nuxt (decided) SPAs are for **logged-in** experiences (customer dashboard, admin). The *public* marketing + funnel pages (the `laff_website` replacement) need SEO and fast first paint, which a client-rendered SPA handles poorly โ€” so the public surface is **Nuxt (Vue SSR)** at `www.`, consuming the same API. Decided 2026-06-13. See [deployment-topology.md](deployment-topology.md). ### Staff procedures / SOPs โ†’ `ops.` (๐ŸŸข added 2026-06-23, Vue 3 SPA) A fifth surface for all of LAFF's internal procedures/SOPs (a requirement from Bruce), at **`ops.loveatfirstfight.com`** (rejected `wiki.` as too generic). **Built as its own Vue 3 SPA** โ€” same pattern as `app.`/`admin.` (behind login, no SEO, Sanctum staff-role token, consumes the same API) โ€” so it's just another API client, not a separate stack. Content scope (which SOPs, authoring flow) is still being fleshed out. See [deployment-topology.md](deployment-topology.md) and the decision log. ### API contract conventions (to lock early) - **Auth:** Laravel Sanctum personal-access / SPA tokens. Same scheme for Flutter. - **Versioning:** URL-prefixed `/api/v1/...`; never break v1 once a client ships. - **Shape:** JSON:API-ish or a consistent resource envelope; Eloquent API Resources. - **Docs:** generate an OpenAPI/Swagger spec so the Vue and Flutter teams share one source of truth (and can codegen typed clients). - **CORS / domains:** `api.` serves `www.` (Nuxt), `app.` (customer), `admin.` (staff), and `ops.` (staff procedures, Vue SPA). Full topology, CORS list, and auth implications in [deployment-topology.md](deployment-topology.md). --- ## Module breakdown (the "domains" inside Laravel) ``` app/ โ”œโ”€โ”€ Identity/ members, staff, roles, auth, SSO โ”œโ”€โ”€ Catalog/ products, bundles, pricing โ”œโ”€โ”€ Courses/ courses โ†’ modules โ†’ chapters โ†’ lessons โ†’ resources, drip, certificates โ”œโ”€โ”€ Commerce/ orders, payments (PSP), refunds, disputes, ledger โ”œโ”€โ”€ Funnels/ quizzes, opt-ins, scarcity timers, landing pages โ”œโ”€โ”€ Automation/ triggers โ†’ conditions โ†’ actions, scheduled jobs โ”œโ”€โ”€ Messaging/ transactional email, ActiveCampaign sync, broadcasts โ””โ”€โ”€ Reporting/ revenue, cohorts, course completion, dashboards ``` Each maps 1:1 to a capability we're retiring (see [../00-index/system-map.md](../00-index/system-map.md) ยง3). --- ## High-level data model (target) ``` User โ”€โ”€โ”€< Order โ”€โ”€โ”€< OrderItem >โ”€โ”€โ”€ Product โ”€โ”€โ”€< Bundle โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€ Payment (PSP) โ”€ Refund/Dispute โ”‚ โ”‚ โ””โ”€โ”€โ”€< Enrollment >โ”€โ”€โ”€ Course โ”€โ”€< Module โ”€โ”€< Lesson โ”€โ”€< Resource โ”‚ โ”‚ โ””โ”€ Progress / Completion โ”€โ”€ Certificate ``` - `Order`/`Payment` separate so one order can have many payment attempts. - `Enrollment` = the grant (replaces `tva_access_history` + manual grants). - `Progress` tracks per-lesson completion (new โ€” TA tracks this in postmeta). See [phasing.md](phasing.md) for build order and [tradeoffs.md](tradeoffs.md) for the decisions behind these choices.