# Decision Log Append-only record of decisions. Newest at top. Each entry: date, decision, why, alternatives, status (🟒 decided / 🟑 leaning / πŸ”΄ revisit). --- ### 2026-06-23 β€” Auth = Sanctum **SPA cookie** for web (supersedes "tokens everywhere") 🟒 **Decided (Berner).** Web logins (`ops.`/`admin.`/`app.`) use **Sanctum SPA session cookies** (stateful, httpOnly), **not** Bearer tokens. Bearer tokens are **reserved for the future Flutter app** β€” Sanctum serves both from one setup. **Supersedes** the 2026-06-12 front-end entry's "Sanctum token-based (Bearer), not cookie/session SPA mode" call. **Why the change:** the session lives in an **httpOnly cookie** JS can't read, so an XSS bug can't exfiltrate it β€” materially safer for an admin/ops surface that controls refunds & customer data. All surfaces are subdomains of `loveatfirstfight.com`, so cookie sharing is trivial (`SESSION_DOMAIN=.loveatfirstfight.com`). Flutter is unaffected (it uses tokens from the same Sanctum guard). **Cost accepted:** SPA must hit `GET /sanctum/csrf-cookie` before login; CORS needs `supports_credentials=true` + an explicit origin allowlist; `SANCTUM_STATEFUL_DOMAINS` lists the web front-ends. **Roles/permissions:** **spatie/laravel-permission** (roles customer / staff / admin / super-admin + granular permissions). **Stack:** Laravel **13** (PHP 8.3+), PostgreSQL, session driver = `database` (no Redis yet β€” lean per hosting decision). --- ### 2026-06-23 β€” Dev environment = local Docker on existing shared Traefik 🟒 **Decided (Berner).** The Super System dev stack runs in **Docker on Berner's PC** (10.10.0.10), fronted by the **already-running shared Traefik v3.6** (`D:\Development\traefik\`, owns :80/:443) that also serves ~30 other stacks β€” we **plug in**, not run our own proxy. **Pattern (mirrors the SuperMatch stack):** each service joins the external `traefik-network`, declares routes via **docker labels** with `tls.certresolver=letsencrypt`; HTTPβ†’HTTPS redirect middleware per host. **Dev domains:** `dev.{api,app,admin,ops,www}.loveatfirstfight.com`. **TLS:** real Let's Encrypt via the existing **HTTP-01** resolver β€” which requires the dev names to be **public** A records β†’ **`156.155.180.202`** (the public IP, hairpins back to the PC), exactly like every other dev project here. ⚠️ As of this writing the LAFF dev names resolve only **internally** to the gateway `10.10.0.254` with **no public record**, so certs won't issue until the public records are added. **Repo layout:** `super system/docker-compose.yml` + `super system/docker/…` + `super system/api.loveatfirstfight.com/` (Laravel). Detail: [../03-architecture/deployment-topology.md](../03-architecture/deployment-topology.md). --- ### 2026-06-23 β€” New surface: `ops.` (Vue 3 SPA) for staff procedures / SOPs 🟒 **Decided (Berner).** Adds a **fifth surface** to the four-domain topology: a staff-facing home for all of LAFF's procedures/SOPs (a requirement from Bruce β€” "a place where all their procedures and everything is"). **Subdomain:** **`ops.loveatfirstfight.com`**. **Stack:** **own Vue 3 SPA** β€” same pattern as `app.`/`admin.` (behind login, no SEO, Sanctum staff-role token, consumes the same `api.`). **Why `ops` over `wiki`:** "wiki" implies a generic wiki tool and undersells it; `ops` reads as the staff operations/procedures home. **Why build (own SPA) over buy (off-the-shelf wiki) or fold-into-`admin.`:** ownership and one consistent staff stack; rejected an off-the-shelf wiki tool and a sub-module of `admin.`. **Implications:** fourth front-end origin in the CORS allowlist; staff-role scope. **Still being fleshed out (not blocking):** content scope β€” which SOPs, how authored/structured. **Status of disk:** `super system/wiki.loveatfirstfight.com` renamed β†’ `ops.loveatfirstfight.com`; missing `admin.loveatfirstfight.com` folder created so disk matches the topology. Topology + architecture docs + dashboard updated; the ASCII diagrams still show the original four β€” redraw to add `ops.` as a fifth front-end. Detail: [../03-architecture/deployment-topology.md](../03-architecture/deployment-topology.md). --- ### 2026-06-13 β€” DB topology revised: two databases, one server (not two servers) 🟒 **Decided (Berner).** Supersedes the earlier "separate PG servers" call. Functional (`laff`) and audit (`laff_audit`) are **two databases on ONE Postgres server** now, with the app using a **separate connection for audit** (split-aware) so promoting `laff_audit` to its own server later is an infra change with **zero app code change**. **Why the change:** real load is tiny β€” ~88 orders/day, ~20 signups, a few hundred audit events/day (<1 write/sec). Two physical servers would isolate a *trickle*; a single box already serves ~40–60k sessions/day. It was right thinking applied to load we don't have. **Kept:** clean separation, the same no-JOIN discipline, a clean lift-and-shift seam. **Give up:** cross-database JOINs (same as the two-server plan β€” nothing new lost). **Split trigger (later):** audit writes showing in functional CPU/IO; `laff_audit` backups/vacuum hurting the main DB window; or wanting hard risk-isolation (a disk/tablespace quota can cover that without splitting). **Cost effect:** start stack drops from ~$60 β†’ ~$45/mo (one PG box, not two). Same "build lean, split on a measured signal" pattern as Redis / PgBouncer / EC2 sizing. Detail: [../03-architecture/data-stores-and-logging.md](../03-architecture/data-stores-and-logging.md). --- ### 2026-06-13 β€” Reconciled with Bruce's Knowledge Base 🟒 **Decided (Berner).** Found an existing Super System spec in the Company Knowledge Base (Berner & Bruce). Reconciled it with our research: - **Video β†’ BunnyStream** (closes the video-storage blocker; the ~$2,800 AWS cost was video, not DB backups). - **DB = PostgreSQL** (KB had it as "TBD" β€” our decision resolves it). - **WAL-G β†’ S3 PITR stays**, with Berner's local RAID + international as extra copies. - **Adopted from KB (new requirements):** one-product-many-pricing-options (retires ~18+8 duplicate products), time-zone-aware everything, gamification (couple/partner progress), CRM with per-user "3D" column views, Amazon SES email + per-minute scheduler, editable-marketing-vs-locked-logic, funnel builder, attribution (absorb ClickMagick), QuickBooks/Wave sync, AI-native APIs, sockets. - **Commercials:** ~$7k/yr tool savings; **backstop = AC renewal 4 Jun 2027**; dispute-evidence tool = "first win". - **Still open:** phasing (Trust-Again pilot vs back-end-first β€” decide w/ Linka), password manager, Stripe-primary confirm. Detail: [../03-architecture/reconciliation-with-bruce-kb.md](../03-architecture/reconciliation-with-bruce-kb.md) and [../04-features/business-requirements-from-bruce-kb.md](../04-features/business-requirements-from-bruce-kb.md). --- ### 2026-06-12 β€” Research-first, no code yet 🟒 **Decided.** Build a `research/` knowledge base before any production code. **Why:** This replaces a live ~$7.9M business; we need a shared map first. **Alternatives:** Start prototyping immediately. Rejected β€” too risky blind. --- ### 2026-06-12 β€” Backend: Laravel 🟑 **Leaning (Berner's stated preference).** **Why:** Mature, batteries-included (auth, queues, jobs, mail, Eloquent), great fit for a content+commerce+automation system; strong migration tooling. **Alternatives:** Node/Nest, Django, Rails. Revisit only if a hard constraint appears. --- ### 2026-06-13 β€” Payments = Stripe (primary) + PayPal; drop ThriveCart 🟒 **Decided (Berner).** Process payments directly through **Stripe** (primary β€” Apple/Google Pay, Klarna, cards) and **PayPal** (second), via LAFF's own accounts. No more ThriveCart. **Data backs it:** today's paid revenue is Stripe ~$4.04M (51%), ThrivePay ~$2.12M (27%), PayPal ~$1.76M (22%). **"ThrivePay" is ThriveCart's white-labelled wrapper over Stripe/PayPal** β€” it leaves with ThriveCart and re-routes through our own accounts; NOT a third gateway. So Stripe+PayPal direct covers 100% of current behaviour, nothing orphaned. Refunds from inside the system; Stripe+PayPal webhooks are the only inbound webhooks we keep. **Open:** recurring/ subscriptions scope, saved-card/token migration, single-currency confirm, own live PSP accounts. Highest-risk migration (Phase 3) β†’ dual-run + daily reconcile + rollback. Detail: [../04-features/payments.md](../04-features/payments.md). --- ### 2026-06-13 β€” EC2 sizing & scaling tiers (capacity per instance) 🟑 **Planning model.** Mapped EC2 instances β†’ daily capacity. Because the 536 MB dataset fits in RAM, scaling = adding vCPUs; each tier ~doubles capacity. **Current real load (measured):** ~3.3k visits/day, ~88 orders/day, ~20 signups/day, ~544 analytics events/day β†’ ~5–6 q/s peak = **~1–2% of the smallest sensible box.** **Recommended start (sized to reality, Berner):** Postgres `t3.medium` (2 vCPU/4 GB, ~$30/mo) β†’ **~40k–60k sessions/day** (~15Γ— current); audit DB `t3.small`; Redis `t3.small`. Whole start stack **β‰ˆ ~$60/mo.** Move main DB to `m7g.large` only on a measured signal (sustained CPU / CPU-credit exhaustion on a webinar spike). Ladder: `m7g.large` β†’ ~150k–200k/day, `m7g.xlarge` β†’ ~400k/day, `m7g.2xlarge` β†’ ~800k/day, `r7g.4xlarge` β†’ ~1.5–1.9M/day. **A read replica doubles read capacity more cheaply than a tier jump + doubles as failover standby.** Redis sized for RAM (cache + stream), not CPU. Lever-by-signal: DB CPUβ†’replica/tier; connections maxedβ†’PgBouncer; Redis evictingβ†’bigger/split; API CPUβ†’autoscale. **CPU vs connections (two different DB limits):** CPU scales with instance size (bigger EC2 / read replica); **connections do NOT** β€” each PG connection is an OS process (~5–10 MB), and >~few hundred *lowers* throughput. The fix for the connection ceiling is **PgBouncer** (transaction-pooling: multiplexes many API connections onto a small real pool). Pressure comes from autoscaling Beanstalk API nodes each holding a pool. **Add PgBouncer when the API autoscales wide** (trigger: active_connections near max, or >~3–4 nodes), not day one. ⚠️ Modelled estimates β€” validate with a load test. Detail: [../03-architecture/ec2-sizing-and-scaling.md](../03-architecture/ec2-sizing-and-scaling.md). --- ### 2026-06-13 β€” Hosting = AWS; DB on EC2 + WAL-G PITR; bottleneck = Postgres 🟒 **Decided (Berner).** AWS: **CloudFront** for `www`/`app`/`admin` (SPAs are static; Nuxt SSR on small EC2 behind CF), **Elastic Beanstalk** for the Laravel API (autoscaling; ECS Fargate noted as modern alt), **EC2 PostgreSQL** and **EC2 Redis**, **S3** for media/backups/assets. **DB safety on a budget:** rejected RDS on cost. Self-managed Postgres on EC2 with **WAL-G/pgBackRest streaming WAL β†’ S3 = point-in-time recovery to any second** for S3-storage cost only; optional `spatie/laravel-backup` dump as a portable 2nd copy. Accepted trade-off vs RDS: **manual failover + runbook** (optional cheap streaming replica later for fast failover). Redis = AOF persistence, rebuildable, no backup needed (spatie doesn't cover Redis β€” and doesn't need to). **Capacity finding:** front end (CloudFront/SPAs) is NOT the bottleneck β€” it scales ~infinitely. **The DB is the ceiling (CPU + connections).** Functional dataset is only **~536MB β†’ fits in RAM**, so DB is memory-served, not disk-bound. A single small EC2 Postgres comfortably serves **~100k+ member sessions/day (~30Γ— current ~3.3k/day traffic)**. Writes are trivial (~60–90 orders/day, peak 188). Scaling levers in order: cache β†’ read replica β†’ PgBouncer β†’ vertical. Full model: [../03-architecture/hosting-and-capacity.md](../03-architecture/hosting-and-capacity.md). --- ### 2026-06-13 β€” Queues = Horizon + Redis; analytics via Redis Stream; split by traffic 🟒 **Decided (Berner).** Background work = **Laravel Horizon + Redis**. Two Horizon worker pools: **ops** (webhooks/mail/certs/drip β€” high prio, reserved processes) and **analytics** (low prio, high volume). Analytics events buffer through a **Redis Stream**, drained in **batches** (not one job per event). **Isolation is staged by MEASURED traffic, not pre-built:** - Worker/CPU isolation (separate pools) β‰  Redis memory/throughput isolation (separate connectionβ†’instance). Different layers; climb each on a trigger. - **LAFF is at Stage 0:** ~100k clicks/month β†’ projected full clickstream ~1.5M events/mo (~50k/day, ~10/sec peak). One Redis handles this with huge headroom. A separate Redis now would be premature (~3–4 orders of magnitude below capacity). - **Split triggers (watch):** Redis `evicted_keys > 0` (#1), memory near maxmemory, ops ceiling, ops-pool wait time, stream backlog growing. Volume gates ~50k/day β†’ separate logical DB; ~1M/day β†’ separate Redis instance; ~10M/day β†’ +ClickHouse / maybe Kafka. - Named Redis connection + Stream behind `analytics.track()` β‡’ every split is **config-only**, no code change. Full detail: [../03-architecture/queues-and-redis.md](../03-architecture/queues-and-redis.md). --- ### 2026-06-13 β€” Database = PostgreSQL; functional/audit/analytics separated 🟒 **Decided (Berner).** Engine: **PostgreSQL** (over MySQL β€” stricter types, JSON, partitioning, analytical SQL; WPβ†’PG is a one-time ETL anyway). Three workloads, deliberately separated: 1. **Functional** (users, orders, courses, payments) β†’ main PG server (sacred). 2. **Audit / business events** (access history, activity, order-status events, automation runs) β†’ ~~separate PG server~~ **[SUPERSEDED 2026-06-13 β€” now a separate *database* on one server; see the newer entry at top of log]**. 3. **Behavioral analytics** (navigation, A/B tests, funnels, lesson-watch) β†’ **in-house decoupled pipeline**: collector β†’ queue β†’ async workers β†’ store. Store is Postgres now, **ClickHouse later** on a measured trigger. **Driver:** Berner wants heavy analytical/A-B/navigation tracking without slowing the system. Data shows logs already dwarf functional data (`wp_thrive_reporting_logs` = 501MB vs orders = 31MB). **App/debug logs** β†’ files + error tracker, **not** SQL. πŸ”’ **The rule:** analytics writes are NEVER synchronous in a request path β€” always fire-and-forget to the queue. Full detail: [../03-architecture/data-stores-and-logging.md](../03-architecture/data-stores-and-logging.md). **Build vs buy:** build analytics in-house (rejected PostHog) for full ownership. --- ### 2026-06-13 β€” Deployment topology: four domains, one API 🟒 **Decided (Berner).** `www.loveatfirstfight.com` = Nuxt (public, SSR/SEO); `app.loveatfirstfight.com` = customer Vue SPA; `admin.loveatfirstfight.com` = staff Vue SPA; `api.loveatfirstfight.com` = Laravel JSON API consumed by all three (and the future Flutter app). **Why:** clean separation, one contract, SEO where it matters. **Implications:** Sanctum token auth, CORS allowlist for the three origins, Nuxt does server-side API calls for public data. Full detail: [../03-architecture/deployment-topology.md](../03-architecture/deployment-topology.md). Resolves the "public site SSR or defer" open question β†’ **Nuxt now**. --- ### 2026-06-12 β€” Front-end: decoupled JSON API + two Vue SPAs 🟒 **Decided (Berner).** Laravel is a **pure JSON API**; a separate **customer Vue SPA** and **admin Vue SPA** both consume it. **Why:** a Flutter app is planned later β€” a decoupled API is the single contract that web *and* mobile reuse with zero re-architecture. **Alternatives rejected:** Inertia (couples web to Laravel, no mobile reuse) and Filament admin (not reusable by Flutter). **Accepted costs:** build auth ourselves (Sanctum), version+document the API (`/api/v1`, OpenAPI), hand-build the admin SPA (mitigate with a Vue component library). **Nuance:** public marketing/funnel pages want SEO β†’ build that surface as Nuxt/SSR (or defer), still on the same API. --- ### 2026-06-12 β€” Payments: TBD 🟑 Likely keep **ThriveCart** during early cutover (don't break cash flow), then migrate to a directly-integrated PSP (Stripe and/or PayPal). **Not decided.**