Hermes Wiki
LocalzDocs/Stack-Finalization-Draft

Localz Stack — Finalization Draft (working doc)

Temporary/living doc. We edit this in place as we discuss, then fold the final result into Tech-Stack.md / Localz-Required-Stack.md once settled. Delete or archive this file after finalization.

Stated priorities (in order)

  1. Open-source — high priority
  2. Secure — second priority
  3. High availability — third priority
  4. Low-ops — fourth priority (lowest weight; willing to accept more ops burden if it serves 1–3)

This is a different weighting than the existing Tech-Stack.md / Localz-Required-Stack.md, which lean toward managed SaaS (Cognito/Clerk, Mongo Atlas, MSK, EKS) for low-ops-first reasons. This doc re-derives the stack under the new ordering — reconcile back into those docs once decided.

Phase definitions

Referenced by user count (not calendar date) throughout this doc — a phase transition is triggered by hitting the range, not by a deadline.

Phase Scale Ops posture RTO target (full-VPS-loss recovery)
Phase 1 — Launch 10s–100s of users Single person running everything. Budget-constrained — no hires, no premium managed add-ons beyond what's already decided above (Supabase, Stripe, Resend). Single VPS. Accept some HA gaps in writing rather than over-building for load that doesn't exist yet. ~4 hours, manual — rehearsed but human-driven procedure (see Pre-launch checklist below), not automated failover
Phase 2 — Growth 1,000s–10,000s of users Expected to be monetized enough by this point to afford either hiring (even just a contractor/part-time hire) or upgrading to more managed/HA-grade services (e.g. Redis Sentinel/Upstash, Postgres failover tier) — but still realistically a single-person-run startup day to day. Tiered VPS split (web/worker/search/observability/edge, see Phase 2 tier breakdown below). This is where the Phase 1 HA gaps from the checklist below get closed for real, not just documented. Zero data loss becomes a hard requirement from here on — both data already in the DB and data mid-processing at the moment of a crash (see "Data durability guarantee" section below). ~15 minutes, semi-automatic — a single web-tier box dying no longer takes the app down (2+ web-tier boxes behind Traefik, Coolify restarts/reroutes); losing a whole tier (e.g. the observability box) still needs a manual rebuild, but the app itself stays up
Phase 3 — Scaled Beyond Phase 2 The target steady-state operating model, not a new architecture layer — same OSS/Coolify/Hetzner foundation, matured: automatic failover (not manual "spin up within hours"), redundancy at every layer flagged as a gap in the HA/CAP table below, and alerting/observability mature enough that no incident goes unnoticed — everything that's a documented accepted-risk at Phase 1 is closed out by here. Zero-data-loss guarantee from Phase 2 carries forward unchanged, now running at higher volume. Seconds–low minutes, automatic — no single box loss (any tier) causes user-visible downtime; recovery is a health-check/restart event, not a human-run runbook

Locked-in so far

  • Object storage: AWS S3 or Cloudflare R2 (decide later)
  • SQL DB: Supabase (Postgres, open-source core, self-hostable if needed)

Open questions / decisions to make

Layer Candidate(s) Status
Backend framework FastAPI (Python) decided
Frontend Next.js decided
Auth Supabase Auth (GoTrue, OSS) — web, PWA, and mobile (React Native/Expo) via @supabase/supabase-js with a platform-specific storage adapter decided
Search Meilisearch (OSS) — user-facing NL search across services/products/location decided
Cache / queues Redis (cache) + Celery (task queue, Redis as broker) decided — BullMQ ruled out (Node/TS-only, incompatible with FastAPI/Python). Note: RabbitMQ was the broker discussed for stronger durability/HA; Redis chosen here for simplicity — revisit if delivery guarantees become a problem in practice.
Payments Stripe (Connect for marketplace split payouts) decided — no real OSS alternative for PCI-compliant rails, accepted as necessary exception
Compute/hosting Phase 1 (10s–100s users): Coolify (OSS PaaS) on one Hetzner VPS — no standby, failover is manual (see Pre-launch checklist). Phase 2 (1,000s–10,000s users): stay on VPS, but split into multiple dedicated VPS by tier (web, worker, search, observability, edge — see Phase 2 tier breakdown below), still orchestrated via Coolify's native multi-server support. Phase 3 (scaled): same tiered-VPS foundation, matured to automatic failover at every layer instead of a manual rebuild procedure decided — revised from an earlier ECS/EKS/Fargate Phase 2 plan. Staying VPS-based (not migrating to managed container orchestration) keeps priority #1 (open-source) and #4 (low-ops via Coolify) intact; splitting by tier still gets the HA/isolation benefit (a Grafana-stack crash doesn't take down the app server, worker CPU bursts don't steal web-tier capacity) without adopting AWS-proprietary tooling. Revisit ECS/EKS only if VPS-tier scaling genuinely hits a ceiling Coolify can't manage.
Reverse proxy/edge Traefik (OSS, ships built-in with Coolify) decided
Observability Grafana + Prometheus + Loki + Sentry + OpenTelemetry decided — same stack for all phases, not just MVP. OpenSearch not needed (Meilisearch already covers search, Loki already covers logs; adding OpenSearch would duplicate both roles with extra ops). Sentry: use hosted SaaS free/small tier for MVP, self-host later — self-hosted Sentry bundles Kafka + ClickHouse + its own Postgres/Redis internally and needs 16GB+ RAM on its own per Sentry's docs, disproportionate to the rest of the stack at Phase 1 scale. Grafana/Prometheus/Loki/OTel deliberately kept self-hosted long-term (not swapped for a managed observability SaaS) — owner wants to do the devops work directly, and the resulting metrics/dashboards double as business-insight signal feeding future product/dev decisions, not just uptime monitoring.
Hosting region Supabase project currently in eu-central-1 (leftover from when VPS was in Germany) decided — create a new Supabase project in a US region (e.g. us-east-1) rather than migrating the existing one in place (Supabase doesn't support in-place region migration); do this now while the project has no real data. VPS itself also needs to move from Germany to a North American region — see below.
VPS sizing (Phase 1) Core app (FastAPI+Next.js+Redis+Celery+Meilisearch+Traefik+Grafana/Prometheus/Loki) vs a beefier spec decided: 8 vCPU / 16GB RAM / ~200GB SSD is enough, provided Sentry is not self-hosted on the same box. Product data lives off-box already (Postgres→Supabase, media→S3/R2), so this VPS is neither compute- nor storage-heavy in the way a typical monolith host would be — the only thing that would force 16 vCPU/32–64GB/500GB territory is co-locating self-hosted Sentry (Kafka+ClickHouse) on it. Keep Loki/Prometheus retention short (7–14 days) to keep disk flat. Revisit once real traffic/log-volume numbers exist.
VPS provider/region Hetzner Ashburn/Hillsboro (US) only offers the pricier dedicated-vCPU (CCX) line, no cheap shared-vCPU (CX) tier in the US decided: Hetzner, US region (Ashburn/Hillsboro) — chosen for trustworthiness/track record over OVH/DigitalOcean/Vultr, accepting the CCX (dedicated-vCPU) price premium over Hetzner's cheaper CX line as the cost of staying with a provider judged more reliable. Applies to Phase 1's single VPS and, by extension, each dedicated VPS added per-tier in Phase 2+ unless a specific tier's workload argues otherwise.
Email/notifications Sending: Resend/Postmark (SaaS); Listmonk kept self-hosted for the campaign/newsletter UI only, not for sending revised — see managed-service criteria below; self-hosting a mail server buys no real "control" since deliverability depends on Gmail/Outlook trusting your IP regardless of who runs the server
Redis (revisit) Self-hosted vs Upstash (serverless Redis-compatible) conditional — see managed-service criteria below
Mongo vs Postgres Postgres-only in production — JSONB for flexible/dynamic-schema content (replaces Mongo's role), pgvector for embeddings, plus users/bookings/auth all in the same Postgres (via Supabase) decided — single datastore in production is simpler to secure/harden/cluster than running Mongo alongside Postgres. (Flag if Mongo was actually meant to stay for local prototyping only, not production.)
Event streaming Kafka — but skipped entirely for MVP (no event-streaming use case yet); adopt Kafka specifically (not Redpanda) whenever a real multi-consumer/replay need shows up decided
On-demand/serverless compute Deferred — Celery worker tier covers it for now. Candidates if a real need emerges: OpenFaaS/Knative (OSS, self-hosted, needs k3s) for scale-to-zero; AWS Lambda accepted as a narrow non-OSS exception (like Stripe) only for stateless glue — e.g. S3/R2 upload→thumbnail resize, webhook signature verification — not core business logic. Step Functions only relevant if Lambda is already adopted (it's an orchestrator, not compute) — candidate for multi-step workflows like Stripe Connect payout reconciliation deferred — revisit if media-processing bursts or webhook volume outgrow what the always-on worker tier handles well
Orchestration platform (Coolify vs Kubernetes) Coolify (multi-server, Phase 1 + Phase 2 tier split) vs Kubernetes (k3s) decided: stay on Coolify — it already covers what's needed (per-tier VPS isolation, multi-server deploy, restart-on-crash HA) without K8s's ops burden (cluster upgrades, CNI, RBAC, etcd). Revisit only if: (a) self-hosted FaaS (OpenFaaS/Knative) is adopted for on-demand compute above — Knative specifically requires K8s, or (b) real load-based autoscaling (not just "add a box manually") becomes necessary. Neither is true yet at current scale.
Multi-user read/write independence Connection pooling (Supabase Supavisor) + Redis read caching now; Postgres read replicas deferred decided — adopt pooling + Redis caching now (cheap, already in the stack: pooling is non-negotiable at any scale to avoid exhausting Postgres connections across multiple FastAPI instances; Redis cache-aside on hot/infrequently-changing reads decouples most reads from the write path with zero new infra). Defer read replicas until real read-volume numbers justify them — they add replica-lag complexity (a write may not be immediately visible on a replica) that isn't worth taking on speculatively.
Prod feature rollout to a subset of users GrowthBook (OSS, self-hosted, Postgres-backed) vs PostHog (OSS, self-hosted, ClickHouse-backed) vs Traefik weighted routing decided: GrowthBook, permanently — not a Phase 1-only pick. Its ops footprint doesn't scale with traffic (it evaluates flags via SDK locally and queries your existing Postgres/warehouse for experiment stats, rather than ingesting an event firehose itself), so there's no scale at which it needs to be replaced. Percentage/user-attribute-targeted rollout, instant on/off with no redeploy, Python SDK (FastAPI) + JS SDK (Next.js) gate both backend logic and frontend UI. PostHog is additive, not a replacement — a separate later decision for product analytics/session replay, to sit alongside GrowthBook once there's a dedicated ops tier (e.g. the Observability VPS tier) to absorb its ClickHouse dependency; its ops burden scales with event volume, same profile already rejected for self-hosted Sentry. Note: Traefik's weighted routing is a separate, infra-level capability (splits raw traffic across two whole deployed builds, no per-user targeting) — not a substitute for GrowthBook, just a free complementary tool already available for whole-build canaries if ever needed.

Managed-service acceptance criteria

Not a blanket "managed vs self-hosted" call — apply this test to any data/IO-bound piece. Accept a managed service only if it clears all three:

  1. Cheaper than the ops burden of self-hosting it at current scale.
  2. Full control retained — data export/backup on demand, configurable security (encryption, access scoping, network restriction), realistic to swap out later (no hard lock-in).
  3. Feeds central observability — exposes Prometheus-scrapeable/OpenTelemetry metrics directly, or emits events (webhooks/logs) that can be forwarded into the self-hosted Grafana/Loki stack. A pure black-box dashboard with no export fails this.

Mechanism for centralizing observability across managed + self-hosted pieces: an OpenTelemetry Collector on the observability VPS — scrapes Prometheus endpoints, receives OTLP pushes, and ingests forwarded webhook events, all landing in the same Grafana.

Applied:

Service Verdict Why
Supabase (Postgres) Passes Standard Postgres underneath — pg_dump anytime, direct SQL access, no lock-in. Verify it permits pg_stat_statements/an exporter so postgres_exporter can feed Prometheus — don't assume.
S3/R2 Passes S3-compatible API either way, trivially swappable between the two, both emit request/access metrics.
Redis → Upstash Conditional Protocol-compatible, so self-hosting later is a one-line REDIS_URL change — low lock-in regardless. Verify before committing: does it expose Prometheus/OTel-compatible metrics (vs. only its own dashboard)? Does its security model offer network restriction, not just a token? If either fails, self-host Redis instead — it's cheap/simple enough that self-hosting isn't a real burden here, unlike Postgres.
Email sending → Resend/Postmark Passes (firmly) Self-hosting buys no real control here — deliverability depends on IP reputation/mailbox-provider trust, not who runs the server. Both expose delivery-event webhooks (sent/bounced/complained) — forward into Loki as structured logs for centralized observability.

Switching / adding VPS — how easy is it?

Easier than a typical VPS deploy, because state was deliberately kept off the box:

  • Stateless/portable: FastAPI, Next.js, Celery workers — just Docker images, run identically anywhere. Coolify natively supports adding multiple servers to one dashboard and deploying the same app to a new one — this is the mechanism for both "switch providers" and "add more VPS for HA."
  • Not on the VPS at all: Postgres (Supabase), object storage (S3/R2) — switching/adding a VPS doesn't touch these.
  • On the VPS but low-stakes to lose: Redis (ephemeral cache/broker — cold cache and idempotent job retries are the only cost), Meilisearch (index rebuildable from Postgres, or snapshot/restore).
  • Procedure: spin up new VPS → install Docker + Coolify agent → redeploy same app definitions → rebuild/restore Meilisearch → cut over DNS (low TTL / Cloudflare proxy swap) → decommission old box.

Phase 2 tier breakdown

Decided: stay on Hetzner VPS (same provider/region as Phase 1), add a dedicated VPS per tier rather than one-VPS-per-tool or migrating to ECS/EKS. Trigger for splitting is adding a second app server (expected around the Phase 2 range — 1,000s–10,000s users, see Phase definitions above), not a calendar date. Coolify's native multi-server support is the deploy mechanism for every box below.

Tier Contents Provider Why separate
Managed/off-box Supabase (Postgres+Auth), S3/R2, Stripe, Upstash (Redis, config-swappable — see criteria above), Resend/Postmark (email sending) n/a (SaaS) Chosen for cost + reversibility + observability-export, not "less ops" alone
Web tier (1+ VPS) FastAPI + Next.js Hetzner Stateless — the tier horizontally scaled behind the load balancer
Worker tier (1+ VPS) Celery workers Hetzner Media-processing (ffmpeg) jobs are CPU-bursty; co-located with web they'd steal CPU from in-flight requests. Separated, worker capacity scales independently of web traffic
Search tier Meilisearch Hetzner (co-located with web tier initially) Lightweight — co-locate with web tier until query volume/catalog size actually contends for resources
Observability VPS (dedicated) Grafana + Prometheus + Loki + Sentry(self-host, revisit) + OpenTelemetry Collector Hetzner Must survive an app-server crash (it's the diagnostic tool for that crash); naturally becomes a hub once multiple app/worker boxes exist; its log/metric disk I/O shouldn't compete with app CPU. Collector pulls in Supabase/Upstash/Resend signals too, so one Grafana view covers managed + self-hosted alike. Kept as its own dedicated box specifically because the owner wants to invest in this tier — it's not just diagnostics, it's the business-insight layer too.
Edge/Load Balancer (dedicated small box, or managed LB) Traefik/HAProxy Hetzner (or managed LB) Only needed once 2+ web-tier boxes exist

Per-tier sizing not yet determined — needs real traffic/log-volume numbers before right-sizing each box (see VPS sizing row above, which still only covers the single Phase 1 box).

Migration safety across layers

The governing principle behind most decisions above: keep state off the compute layer, keep protocols swap-compatible, keep app containers plain Docker (not tied to Coolify- or K8s-specific features). That's what reduces most scaling moves to "spin up new box, redeploy same image, cut over DNS." Checked against every migration this plan implies:

Migration Lossless? Why / procedure
Single VPS → tiered VPS (Phase 2 split) Yes Postgres and object storage are already off-box; app containers are stateless and redeploy identically via Coolify multi-server. See "Switching/adding VPS" notes above.
VPS provider/region swap (e.g. off Hetzner) Yes, app layer New VPS → Docker + Coolify agent → redeploy → DNS cutover (low TTL). Meilisearch needs an index rebuild/snapshot-restore; Redis cache just goes cold — both accepted as low-stakes by design, not data loss.
Redis self-hosted ↔ Upstash Yes Protocol-compatible — a REDIS_URL change. Cache is ephemeral either way, so there's no "data" to lose in the first place.
Supabase region (eu-central-1 → US) No — one-time exception Supabase has no in-place region migration; a same-provider region switch is a new project + manual dump/restore. This is exactly why the doc says do it now, before real data exists — after that point it stops being lossless.
Sentry hosted SaaS → self-hosted Mostly yes, historical data doesn't carry New instance, new DSN. Past error events don't auto-transfer, but error history isn't treated as business-critical to retain, so this is accepted.
Coolify → Kubernetes (only if triggered per Orchestration Platform row) Not automatic Docker images themselves are portable — that part's free. But Coolify's proxy rules, env/secrets config, and networking are not K8s-native and would need to be re-expressed as manifests/Helm charts. Budget this as real migration work, not a redeploy, if it's ever triggered.
Celery → OpenFaaS/Knative (only if on-demand compute is adopted) Not automatic Celery tasks are Python functions called through a task queue; FaaS handlers are a different invocation model. Treat as new development for whichever tasks move, not a lift-and-shift — pick candidates (e.g. thumbnail generation) deliberately rather than migrating everything at once.

Net effect: every migration on the "yes" side of this table is already covered by decisions made elsewhere in this doc (off-box state, protocol-compatible services, plain Docker). The two flagged exceptions (Supabase region, and the conditional Coolify→K8s / Celery→FaaS moves) are the only points in the current plan where "simple and lossless" isn't free — worth keeping in mind rather than assuming everything scales for free.

HA / CAP posture

Governing pattern: CP (consistent) at the data layer, AP (available) at the stateless layer — not a single uniform CAP choice. Bookings/payments must never be inconsistent (no double-booking, no half-committed payment), so Postgres transactions hold strong consistency there. Availability comes from horizontally redundant stateless app instances that can fail individually without losing data, not from relaxing consistency on the data itself. This is the standard, correct shape for a marketplace handling money, and it's already implicit in decisions made elsewhere in this doc.

Layer HA today? CAP posture Gap?
Postgres (Supabase) Depends on plan tier — automated backups/PITR ≠ automatic failover CP — strong consistency, correctly prioritized for booking/payment data Verify, don't assume — confirm the Supabase tier actually includes an automatic-failover standby, not just restorable backups
App layer (FastAPI/Next.js) Yes, by design — stateless, multiple instances behind Traefik, Coolify restarts dead ones Effectively AP — no state lives here to be inconsistent None
Redis (cache + Celery broker) No — single instance in Phase 1. Fine as a cache (already documented as low-stakes-to-lose); as the Celery broker, a crash mid-job can silently drop or stall in-flight tasks Currently neither CP nor AP — just a single point of failure Real gap given HA is priority #3, above low-ops #4 — fix with Redis Sentinel (self-hosted, OSS-consistent) or Upstash's managed replication, specifically for the broker role
Meilisearch No — single instance, index rebuildable from Postgres AP-tolerant by design — brief unavailability/staleness during rebuild isn't business-critical Acceptable as-is, already scoped as low-stakes
S3/R2 Yes — multi-AZ/durable by the provider N/A, handled for you None
Observability (Grafana/Prometheus/Loki) No — single dedicated VPS in the Phase 2 tier plan N/A — but the exact tool used to diagnose an outage can itself be down during one Worth flagging; not urgent at current scale, revisit once actually depended on operationally

Priorities-consistent action: since the stated order explicitly accepts more ops burden to serve HA over low-ops, the Redis-broker gap is the one worth closing sooner rather than later — Postgres HA needs verification (not new work), Meilisearch and observability SPOFs are acceptable to leave until real dependence on them grows.

Data durability guarantee (Phase 2 onward)

Decided: from Phase 2 onward, zero data loss is a hard requirement, not a best-effort goal — covering both data already committed to the DB and data mid-processing at the moment of a crash. Phase 1 tolerates some of this as documented accepted risk (see Pre-launch checklist); Phase 2's launch gate is exactly where that tolerance ends, because real user/money volume means a lost booking or a silently-dropped payment event is no longer an acceptable cost of low-ops. Two categories, matching how data actually moves through this stack:

1. Data at rest (Postgres/Supabase)

Already the CP layer per the HA/CAP posture above — the requirement here is making sure that posture is actually true, not assumed:

  • Supabase plan tier must include an automatic-failover standby, not backups-only — this was flagged "verify, don't assume" at Phase 1; by Phase 2 it must be confirmed true or upgraded to a tier where it is.
  • PITR (point-in-time recovery) enabled with a retention window that covers a realistic detection lag (a bad deploy or bug isn't always noticed within minutes).
  • Backup + restore actually rehearsed at Phase 2 scale (not just the Phase 1 single test) — restore time itself becomes a number worth knowing once real user data volume exists.

2. Data in flight (mid-processing at the moment of a crash)

This is the gap Phase 1's architecture doesn't fully close yet — a crash between two steps (e.g. after a DB commit but before the follow-up Celery task is enqueued, or mid-webhook-processing) can silently drop work even though the DB itself never lost anything. Closing it requires:

  • Celery/Redis broker durability — Redis broker HA (Sentinel or Upstash replication, already flagged as the Phase 1 HA gap) is a hard requirement by Phase 2, not optional-if-budget-allows: an unreplicated broker can lose queued-but-unprocessed tasks outright on crash.
  • At-least-once task processing, made idempotentacks_late=True + task_reject_on_worker_lost=True on Celery tasks so a worker dying mid-task requeues it rather than losing it; every task handler (booking confirmation, payout trigger, notification send) must be safe to run twice, since at-least-once delivery means duplicate execution is possible, not just theoretical.
  • Transactional outbox for DB-write-then-async-follow-up flows — anywhere a Postgres write must reliably trigger async work (e.g. "booking committed" → enqueue a Celery job), write an outbox row in the same DB transaction as the primary write, and have a separate relay process move outbox rows into Celery/Redis. This is what closes the gap a plain "commit, then enqueue" sequence can't: a crash or broker outage between those two steps can otherwise drop the event with no trace, even though the booking itself is safely committed.
  • Stripe webhooks persisted before processing — write the raw webhook payload to Postgres (with its Stripe event ID for idempotency) before acting on it, so a crash mid-processing can be replayed from the persisted record instead of depending solely on Stripe's own retry behavior.
  • Atomic file upload commit — for S3/R2 uploads, write to a staging key and only reference it in the DB (the "commit" step) once the upload is confirmed complete, so a crash mid-upload can't leave a DB row pointing at a partial/missing object.

Not required at Phase 1, per the priorities ordering (low-ops is #4) and the explicit accepted-risk framing in the Pre-launch checklist — but every item above should be treated as a hard gate item in the Phase 2 launch checklist below, not a "nice to have."

Pre-launch checklist (review before onboarding real users — even just 10)

Free/solo testing today doesn't need any of this — HA, backups, and failover only start mattering once someone besides you depends on the app working. Use this list as the gate before the first outside user touches production, so it doesn't need to be re-derived from the whole doc under pressure.

Data safety

  • Confirm Supabase plan tier's actual failover behavior (automatic standby vs. backups-only) — don't assume PITR means HA
  • Confirm the Supabase project is on the target US region, not the leftover eu-central-1 one — must happen before real data exists (no in-place region migration)
  • Connection pooling (Supavisor) enabled on the Postgres connection string
  • Take one manual backup + do one test restore, so restore isn't untested the first time it's needed for real

Availability

  • VPS count at launch: 1 (Phase 1 scale — 10s–100s of users — doesn't justify a hot standby box; explicitly accepted here, not an oversight). No automatic failover exists at this phase.
  • In place of a standby, confirm the manual failover / new-box procedure is actually rehearsed, not just documented — the "Switching / adding VPS" section above already lays out the steps (new VPS → Docker + Coolify agent → redeploy same app definitions → rebuild/restore Meilisearch → DNS cutover). Do one dry run before launch and time it — this is what turns "we could spin up a new server" into a real RTO number.
  • Target RTO for a full VPS loss at Phase 1: ~4 hours (bounded mainly by DNS TTL/Cloudflare cutover + Meilisearch rebuild, since Postgres/S3 are already off-box; see Phase definitions table above) — write down the actual dry-run time here once measured: ___
  • Redis broker HA decided and applied (Sentinel or Upstash) — or explicitly accept single-instance risk in writing if deferring past this point
  • Confirm VPS is actually sized/deployed per the locked 8 vCPU/16GB/~200GB spec, on Hetzner US region
  • Note: multi-VPS HA (tiered split, no single point of failure) is a Phase 2 item (1,000s–10,000s users, see Phase definitions above) — not required to pass this checklist

Observability (so day-1 issues are visible, not discovered via user complaints)

  • Grafana + Prometheus + Loki + OTel Collector actually running with at least basic dashboards built — not just installed
  • Sentry hosted SaaS wired into FastAPI + Next.js and verified to receive a test error
  • Alerting configured for at least: app down, error-rate spike, disk usage on the VPS

Payments

  • Stripe switched from test mode to live keys
  • Stripe webhooks pointed at the production URL and verified
  • Stripe Connect payout onboarding flow tested end-to-end with a real (or Stripe test-mode-equivalent) account

Security

  • HTTPS/TLS auto-provisioning via Traefik confirmed working on the production domain
  • Supabase Auth flows (signup/login/password reset, and mobile deep-link redirect if applicable) tested end-to-end
  • No secrets committed to the repo; production .env/secrets stored outside git
  • MFA enforced on Supabase, Stripe, Hetzner, Cloudflare, GitHub, and the domain registrar — see "Access, secrets & audit" above
  • Domain/DNS continuity confirmed: business-controlled email on the registrar account, auto-renew on with headroom, WHOIS privacy + transfer lock on
  • admin_actions audit log table exists and is actually written to by at least one real admin action path (refund, listing removal, or dispute resolution)

Legal & compliance

  • Terms of Service and Privacy Policy published and linked from signup, even in draft/template form — see "Legal & compliance" above
  • Data retention/deletion policy written down, with a real deletion code path (not just a policy sentence)
  • DPAs on file (or requested) for Supabase, Stripe, Resend/Postmark, Twilio, Hetzner

Rollout tooling

  • GrowthBook deployed and reachable if launching any feature behind a flag from day one — otherwise fine to add when the first flagged feature is actually needed, not required before the first 10 users

Phase 2 launch checklist (review before the trigger point — a second app server / real growth beyond Phase 1 scale)

Second gate, same shape as the Pre-launch checklist above — the "two dots" this doc now tracks: Phase 1 gate (first outside user) and Phase 2 gate (this one, first real growth beyond single-VPS scale). Its job is to make sure the money/hiring headroom implied by hitting 1,000s–10,000s users (see Phase definitions above) actually gets spent on closing the Phase 1 accepted-risk items — not just absorbed as margin. Don't start this list from scratch under pressure; walk it once traffic/revenue trendlines suggest the Phase 2 range is close, not only after the single VPS is already struggling.

Trigger check

  • Confirm the actual trigger: adding a second app server because Phase 1's single 8 vCPU/16GB box is genuinely capacity-constrained (CPU/RAM/connection-pool pressure visible in Grafana) — not a calendar date or a guess
  • Confirm revenue/monetization actually supports either a hire or the managed-HA upgrades below (per Phase definitions) before committing to the added monthly Hetzner + managed-service spend — this gate exists so the tier split is a funded decision, not a scramble

Compute — tier split

  • Web tier: 2nd (and beyond) Hetzner VPS provisioned, FastAPI + Next.js redeployed there via Coolify multi-server, both boxes live behind Traefik load balancing
  • Worker tier split onto its own VPS (Celery), separated from web so ffmpeg/media-processing CPU bursts stop contending with request-serving capacity
  • Observability VPS provisioned as its own dedicated box (Grafana/Prometheus/Loki/OTel Collector/Sentry-if-self-hosting-by-now) — must survive an app-tier crash, per the HA/CAP table above
  • Search tier (Meilisearch) reassessed — still fine co-located with web, or split out if query volume/catalog size now contends for resources
  • Edge/Load Balancer box (or managed LB) added now that 2+ web-tier boxes exist, per the Phase 2 tier breakdown table above
  • RTO re-measured against the Phase 2 target (~15 minutes, semi-automatic per the Phase definitions table) — confirm losing one web-tier box no longer takes the app down

Availability — close the Phase 1 accepted risks

  • Redis broker HA actually applied now (Sentinel or Upstash) — this was the one flagged real gap at Phase 1 launch; it should not still be open at Phase 2
  • Postgres HA re-verified at the new Supabase plan tier (or upgraded) if the Phase 1 tier's failover behavior wasn't already automatic-standby grade
  • Re-confirm the "Migration safety" table above still holds at this scale (Single VPS → tiered VPS row) — walk the actual cutover, don't assume it's still lossless untested

Data durability — zero data loss becomes a hard gate from here (see "Data durability guarantee" section above)

  • Data at rest: Supabase automatic-failover standby confirmed active (not backups-only), PITR retention window sized to a realistic detection lag, restore rehearsed at Phase 2 data volume
  • Data in flight: Celery tasks set to acks_late=True + task_reject_on_worker_lost=True, and every task handler audited for idempotency (safe to run twice) — booking confirmation, payout trigger, notification send at minimum
  • Transactional outbox pattern implemented for any DB-write-then-async-follow-up flow (e.g. booking commit → Celery enqueue) — confirm no "commit, then enqueue" sequence still exists unprotected
  • Stripe webhook payloads persisted (with Stripe event ID) to Postgres before processing, verified replayable from the persisted record
  • File upload commit path confirmed atomic — staging key → DB reference only after upload completion, for both S3 and R2 if both are in use
  • Redis broker durability specifically re-checked against the "hard requirement by Phase 2" bar above — not left in the "conditional, revisit" state it was allowed to sit in at Phase 1

People / managed-service decision (per Phase definitions)

  • Decide, in writing: hire (even part-time/contractor) vs. lean further into managed HA services (Upstash, higher Supabase tier) vs. both — don't let this default silently to "founder just does more ops work"
  • If hiring: identify which tier/responsibility gets handed off first (obvious candidate: on-call/observability response, since that's the tier explicitly meant to catch what the founder would otherwise miss)
  • If leaning further managed: re-run the "Managed-service acceptance criteria" test above against any newly-added managed service (cost, control/reversibility, observability-export) — the bar doesn't relax just because there's more budget

Observability — from dashboards to real alerting

  • Alerting expanded beyond the Phase 1 minimum (app down, error-rate spike, disk usage) to cover the new tiers — worker queue depth/backlog, per-box resource exhaustion, LB health-check failures
  • Confirm the observability VPS's own uptime is now itself monitored from somewhere that isn't the observability VPS (avoid the single "who watches the watcher" gap flagged in the HA/CAP table above)

Staging & CI/CD — close the Phase 1 accepted risk

  • Old Phase 1 VPS repointed to become the staging/non-prod environment (separate Supabase project, test-mode Stripe keys, staging. subdomain) — see "Staging / non-prod environment" above
  • CI pipeline (lint/test/build) actually gating merges, not just present in the repo
  • At least one Alembic migration rehearsed against staging before being applied to prod, so the migration path is proven, not assumed

Mobile infra re-check (if the mobile app is live by this point)

  • API versioning (/api/v1/) confirmed actually enforced — backend still serving the previous client version, not just the latest
  • Push notification delivery (Expo → APNs/FCM) verified end-to-end on both platforms
  • EAS OTA channels (preview/production) confirmed separated, so a bad OTA push can't hit every user at once

On-call & incident response

Decided: the founder is on-call, Phase 1 and Phase 2, full stop. No formal rotation, no external on-call tooling (PagerDuty etc.) needed yet — that's real overhead for a team that doesn't exist. Revisit only when a hire actually happens (see Phase 2 launch checklist's "People / managed-service decision"), and even then, on-call handoff is a deliberate decision to make explicitly, not an assumption.

What this does require, so "I'm on-call" isn't just a title with nothing behind it:

  • Alerts (already decided: app down, error-rate spike, disk usage — see Pre-launch checklist) must actually reach the founder in real time — phone notification, not just a dashboard nobody's watching.
  • A short runbook per alert type, written while calm, not improvised during the incident: what "app down" means to check first (systemd status → nginx → Traefik → Postgres connectivity), what "Redis broker down" means to check (Sentinel status if applied, or accept queued-task loss per the Phase 1 accepted risk), what "disk usage" means to do (Loki/Prometheus retention is already short — see VPS sizing row — so this should mean "something's abnormal," not "expected growth").
  • This is deliberately lightweight at Phase 1 — the goal is "the founder can actually respond," not a full incident-management process.

Staging / non-prod environment

Decided: the Phase 1 single VPS becomes the staging/dev-non-prod environment once Phase 2's tiered split happens — not decommissioned. This is the cheapest possible way to get a staging environment: no new spend, and it's already configured identically to how prod used to run.

  • Trigger: the same trigger as the Phase 2 tier split itself (second app server needed) — at that point, the original single VPS is repointed: same Coolify install, same app definitions, pointed at a separate Supabase project (or a schema/branch if Supabase branching is available by then) and test-mode Stripe keys, DNS on a staging. subdomain instead of production.
  • What it's for: rehearsing migrations before they hit prod (directly closes the "no staging to test a migration against" gap), smoke-testing a deploy before promoting it, and dry-running the failover procedure without touching real data.
  • Before Phase 2 (i.e. all of Phase 1): no dedicated staging box — budget doesn't support it and Phase 1 traffic is low-stakes enough that "test locally, deploy carefully" is the accepted trade-off. This is a documented accepted risk, same pattern as the other Phase 1 items above.

CI/CD and database migrations

  • Migrations: Alembic (the standard FastAPI/SQLAlchemy migration tool) — every schema change is a versioned migration file, applied via alembic upgrade head, never a hand-run ALTER TABLE against prod. Non-negotiable from MVP, since a bad hand-run migration is exactly the kind of event the "Data durability guarantee" section exists to prevent.
  • CI: GitHub Actions (or equivalent) on every PR — lint, run the test suite, build the Docker images. Add a migration dry-run step (alembic upgrade head --sql against a throwaway DB, or applied against the staging environment above) once staging exists.
  • Deploy path: merge to main → Coolify redeploy (already the mechanism per the Hermes-style poll/webhook pattern) → for anything touching schema, apply the migration against staging first, confirm, then apply to prod. Skipping the staging step is the accepted Phase 1 risk noted above, not a Phase 2+ practice.

Access, secrets & audit

Decided: required at the Phase 1 launch gate, not deferred — this is cheap to set up and catastrophic to skip, since as a solo founder your own dashboard logins are the company's entire access-control model.

  • MFA enforced on every account that can affect production or money: Supabase, Stripe, Hetzner, Cloudflare, GitHub (repo + Actions secrets), and the domain registrar.
  • Secrets rotation: no fixed calendar cadence needed at Phase 1 solo scale, but every secret must be rotatable on demand (never hardcoded anywhere that isn't the .env/EnvironmentFile pattern already documented in AgentStack/Hermes/VPS_DEPLOY_GUIDE.md), and rotated immediately on any suspected exposure (e.g. an accidental commit, a laptop left unlocked). Move to a real secrets manager (e.g. Supabase Vault, or a dedicated tool) as a Phase 2 item once there's more than one person who needs access to secrets.
  • Admin action audit logging: every admin-privileged action that touches money or user data (refund issued, listing removed, dispute resolved, account suspended) gets written to an append-only Postgres table (admin_actions: who, what, when, target ID) — cheap to add now, and this is exactly the kind of thing that's expensive to retrospectively reconstruct after an incident or a dispute.

Mobile app infrastructure

  • Push notifications: Expo push notification service — since React Native/Expo is already the decided mobile stack, Expo's push service is the natural choice: it wraps both APNs (iOS) and FCM (Android) behind one API, no separate Apple/Google push-credential management to hand-roll.
  • OTA updates: Expo EAS Update, with separate channels (preview / production) — lets JS-level bug fixes and small changes ship without an App Store/Play Store review cycle; anything touching native modules still requires a full store submission.
  • API versioning: URL-path versioning (/api/v1/...) from the first mobile release. This exists specifically because of app store review lag (roughly 1–2 weeks, sometimes longer) — the backend must be able to serve at least the current and previous mobile client version simultaneously, since not every user updates immediately even after a release is approved. Breaking a v1 client the moment v2 ships is not acceptable; deprecate with a real window, not instantly.
  • SMS: Twilio — already the decided provider for proxied voice calls (Secure-Communication-Design.md), extend the same account to SMS for OTP delivery and booking reminders rather than adding a second vendor for a closely related job.

Domain, DNS & cross-provider risk

  • Domain/DNS continuity (required at Phase 1 launch, five-minute fix): confirm the domain registrar account is on a business/founder-controlled email (not a personal throwaway), auto-renew is on with a payment method that has headroom, WHOIS privacy is on, and transfer/registrar lock is enabled. A lapsed domain is a self-inflicted outage with no technical postmortem, just an easily-avoidable oversight.
  • Cross-provider disaster recovery (Hetzner or Supabase itself has an incident/suspends the account): accepted risk through Phase 2, same pattern as the other documented accepted risks in this doc — not solved, but written down rather than silently absent. No multi-cloud/multi-provider redundancy is planned; the mitigations already in place (off-box state, portable Docker images, rehearsed rebuild procedure) reduce blast radius but don't eliminate a provider-level outage. Revisit only if provider-level incidents actually happen or Phase 3 scale makes the exposure unacceptable.

Infra-as-code / environment reproducibility

Decided: start lightweight, not Terraform-from-day-one. The real risk this closes is the Coolify + VPS setup living only in the founder's head — if a box needs rebuilding and the only reference is memory, that's a second, undocumented single point of failure layered on top of the hardware one.

  • Export/document the Coolify server + app configuration (Coolify supports exporting app definitions) alongside the app repo, not just left inside the Coolify UI.
  • The systemd/nginx patterns already documented in AgentStack/Hermes/VPS_DEPLOY_GUIDE.md are the reference for anything running outside Coolify's own management.
  • Formal IaC (Terraform/Ansible) is a Phase 2+ item, revisited if/when there's more than one person who might need to rebuild a box, or once the tiered VPS split makes manual per-box setup genuinely error-prone at that count.

Identity verification & dispute handling

Layer Candidate(s) Status
Provider identity verification (KYC) Stripe Identity decided — Stripe is already the payment/Connect provider, so Stripe Identity reuses that relationship instead of adding a third-party KYC vendor; also keeps identity data inside a vendor already vetted for compliance rather than expanding the surface area.
Dispute/chargeback handling Stripe Connect's native dispute flow (for payment disputes) + a Localz-side "dispute center" (per MVP-to-Phase-2-Roadmap.md's Phase 2 Trust & Safety item, evidence upload/SLA timers) for service-quality disputes that aren't a payment chargeback decided for Phase 2 — Stripe handles the payment-dispute mechanics (required regardless, it's how Connect works); the Localz-side dispute center is new product surface, scoped to Phase 2 per the existing roadmap doc, not required for the Phase 1 launch gate.
Fraud/anomaly detection Deferred deferred — no dedicated tooling yet; the tiered-review design (Strategy Note §3, derived-not-declared trust) is the first line of defense against fake/gamed reviews specifically. Revisit with real transaction volume, not speculatively.

Open — required before the first outside user, not a Phase 2 item. Money and PII are involved from day one (Phase 1 is free-to-use per the Monetization decision, but Supabase Auth accounts and booking/PII data exist regardless of whether payment is charged), so this can't wait for monetization to be figured out.

  • Terms of Service, Privacy Policy — not yet drafted anywhere in the vault.
  • Data retention & deletion policy, and a real deletion mechanism (a user's right-to-deletion request needs an actual code path, not just a policy sentence) — ties directly into the Postgres-only datastore decision, since deletion now means "delete/anonymize across one database," which is simpler than it would be split across Mongo+Postgres.
  • Data Processing Agreements (DPAs) with each vendor that touches user data: Supabase, Stripe, Resend/Postmark, Twilio, Hetzner — most SaaS vendors offer a standard DPA on request; this is a checklist item, not a negotiation, for vendors at this scale.
  • Marketplace tax handling: 1099-K (US) / T4A-style reporting obligations once provider payout volume crosses a threshold via Stripe Connect — Stripe surfaces most of the mechanics, but the obligation to actually file/report is the platform's, not Stripe's.
  • Not this doc's job to resolve — this is flagged here because it blocks the Pre-launch checklist, but the actual drafting (ToS/Privacy Policy language, retention schedule specifics) needs either a template service (e.g. Termly/a lawyer-reviewed template) or direct legal counsel — infra planning can identify the gap, not close it.

Changelog

  • 2026-08-27: Backend framework = FastAPI. Open-source, async, consistent with existing docs (Tech-Stack.md, Localz-Required-Stack.md both already assumed FastAPI).
  • 2026-08-27: Frontend = Next.js. Confirmed, matches existing docs.
  • 2026-08-27: Search = Meilisearch. Confirmed for user-facing natural-language search across services, products, and location — OSS, typo-tolerant, simpler ops than OpenSearch/Elasticsearch.
  • 2026-08-27: Cache/queues = Redis + Celery (not BullMQ). BullMQ is Node/TypeScript-only and can't be used from a Python/FastAPI backend — Celery (Redis as broker) is the Python-native OSS equivalent: mature, well-documented, handles retries/scheduling. (arq considered as a lighter async-native alternative, not chosen — less mature tooling for retries/monitoring.) RabbitMQ was discussed as the more durable/HA-appropriate broker per stated priority order (HA > low-ops), but Redis was chosen for simplicity since it's already in the stack for caching — carries weaker delivery guarantees than RabbitMQ, worth revisiting if that becomes a real problem.
  • 2026-08-27: Auth = Supabase Auth, confirmed to work across web, PWA, and mobile (React Native/Expo) — same JS client, swap storage adapter per platform (browser storage vs AsyncStorage/expo-secure-store), configure deep link for OAuth/magic-link redirect on mobile.
  • 2026-08-27: Payments = Stripe (Connect for split payouts to providers/sellers). No viable OSS alternative for PCI-compliant payment rails — accepted as the one necessary non-OSS exception.
  • 2026-08-27: Compute/hosting = Coolify on current VPS for Phase 1; migrate once VPS capacity is exceeded (~1000 users estimated). Phase 2 target named as DigitalOcean/AWS with ECS, Fargate, or EKS — flagging that these are managed/proprietary, not OSS, which cuts against priority #1 (open-source). A self-managed Kubernetes cluster (e.g. k3s on a few DO/Hetzner VMs, still fronted by Coolify or plain Helm) would keep the OSS-first ordering intact at the cost of more ops — worth actually comparing against ECS/EKS at Phase 2 planning time rather than defaulting to managed now.
  • 2026-08-27: Reverse proxy = Traefik. Ships built-in with Coolify, no need to add Nginx separately.
  • 2026-08-27: Observability = Grafana + Prometheus + Loki + Sentry + OpenTelemetry, all self-hosted OSS. OpenSearch considered and rejected — Meilisearch already handles search, Loki already handles logs, so OpenSearch would only duplicate existing roles at extra ops cost.
  • 2026-08-27: Email/notifications = Listmonk/Postal (OSS), instead of Resend/SendGrid (SaaS).
  • 2026-08-27: Datastore = Postgres-only in production (via Supabase). Mongo dropped from the final production stack — JSONB columns cover the flexible-schema use case, pgvector covers embeddings/vector search. One hardened, clustered datastore beats securing and scaling two. (If Mongo was actually wanted for local dev/prototyping only, that's compatible with this decision — just don't ship Mongo to production.)
  • 2026-08-27: Event streaming = Kafka, decided as the tool for if/when the need arises, but explicitly out of scope for MVP — no event-streaming requirement exists yet at MVP stage.
  • 2026-08-27: Observability refined — Sentry stays hosted SaaS for MVP, self-host deferred until error volume justifies the ops weight (self-hosted Sentry bundles Kafka+ClickHouse+its own Postgres/Redis, needs 16GB+ RAM on its own).
  • 2026-08-27: Hosting region flagged as urgent — current VPS (Germany) + Supabase project (eu-central-1) both add latency for a US/Canada-focused audience. Supabase can't migrate region in-place; create a fresh US-region project now while there's no real data to migrate. VPS should also move to a NA region (Hetzner Ashburn/Hillsboro US only has the pricier dedicated-vCPU line — compare OVH Canada / DigitalOcean / Vultr before committing).
  • 2026-08-27: VPS sizing resolved — Localz's core app is neither compute- nor storage-heavy once Postgres (Supabase) and media (S3/R2) are excluded from the box; 8 vCPU/16GB/~200GB is sufficient for Phase 1, conditional on not self-hosting Sentry on the same box. Keep Loki/Prometheus retention short (7–14 days) to avoid unbounded disk growth.
  • 2026-08-27: Confirmed VPS switching/scaling is low-friction given the current stack — app layer is stateless/containerized, DB and media are already externalized, Coolify handles multi-server deployment natively. Only Redis and Meilisearch hold state on-box, and both are cheap to rebuild.
  • 2026-08-27: Refined the "VPS = compute only" principle into a general managed-service acceptance test (cost + full control/reversibility + observability-export) rather than a blanket managed-vs-self-hosted call. Applied it: email sending moves to Resend/Postmark (self-hosting a mail server buys no real control — deliverability depends on IP reputation, not who runs the server); Redis is conditional on Upstash actually exposing Prometheus/OTel-compatible metrics and a real network-restriction security model — self-host if not. Postgres (Supabase) and S3/R2 already pass the test as-is.
  • 2026-08-27: Phase 2 architecture = tiered split (web, worker, search, dedicated observability VPS, edge/LB) rather than one-box-per-tool. Observability VPS runs an OpenTelemetry Collector as the centralization point, pulling in signals from managed services (Supabase, Upstash, Resend) alongside self-hosted tiers so Grafana stays the single pane of glass.
  • 2026-08-28: Confirmed the Grafana+Prometheus+Loki+Sentry+OTel stack is intentionally the permanent observability choice, not an MVP placeholder — owner enjoys doing the dev/devops work themselves, and wants the resulting metrics/dashboards to double as a business-insight source that feeds future development decisions, beyond pure uptime/error monitoring.
  • 2026-08-28: VPS provider decided: Hetzner, US region, for Phase 1 and Phase 2+. Compute/hosting Phase 2 plan revised — no longer migrating to DigitalOcean/AWS ECS/Fargate/EKS; instead stays VPS-based and splits into a dedicated Hetzner VPS per tier (web, worker, search, observability, edge), matching the Phase 2 tier breakdown table, deployed via Coolify's native multi-server support. Keeps priorities #1 (OSS) and #4 (low-ops) intact while still getting tier isolation/HA. Grafana stack specifically called out as worth its own dedicated VPS given it doubles as the business-insight layer, not just diagnostics.
  • 2026-08-28: On-demand/serverless compute — deferred, current Celery worker tier is judged sufficient. If it becomes necessary (media-processing bursts, webhook volume), preference order is: self-hosted OpenFaaS/Knative (OSS, but needs k3s) first, AWS Lambda only as a narrow non-OSS exception for stateless glue code (thumbnail generation, webhook signature verification) — not core logic. Step Functions noted as relevant only alongside Lambda, for multi-step workflows like payout reconciliation.
  • 2026-08-28: Orchestration platform — decided: Coolify, not Kubernetes, for both Phase 1 and the Phase 2 tier split. Coolify's multi-server support already delivers per-tier isolation and restart-on-crash HA without K8s's cluster-ops overhead. Flagged one specific trigger to revisit: adopting Knative for self-hosted on-demand compute would require K8s (k3s) underneath, since Knative has a hard dependency on it — OpenFaaS doesn't strictly require it (can run on faasd/Docker Swarm) so it wouldn't force this by itself.
  • 2026-08-28: Multi-user read/write handling decided — connection pooling (Supabase Supavisor) adopted now regardless of scale, Redis read caching (cache-aside on hot reads) adopted now to decouple reads from the write path, Postgres read replicas deferred until real read-volume data justifies the added replica-lag complexity.
  • 2026-08-28: Prod feature rollout to a subset of users decided: GrowthBook (OSS, self-hosted, Postgres-backed) over PostHog (heavier — ClickHouse) for Phase 1; supports percentage/user-targeted rollout across both FastAPI and Next.js via SDK. Clarified Traefik's weighted routing is a separate infra-level tool (whole-build traffic split, no per-user targeting) — not an alternative to GrowthBook, just a free complementary option already available.
  • 2026-08-28: Prod feature rollout decision firmed up: GrowthBook is permanent, not Phase 1-only — its ops cost doesn't scale with traffic since it queries existing Postgres rather than ingesting events. PostHog reframed as additive (analytics/session replay alongside GrowthBook), deferred until a dedicated ops tier exists to absorb ClickHouse.
  • 2026-08-28: Added a "Migration safety across layers" audit covering every scaling move implied by this plan. Confirmed most are lossless by construction (state already off-box, Redis/S3 protocol-swappable, plain Docker containers) — VPS provider swaps, single→tiered VPS split, and Redis↔Upstash all fall out of decisions already made elsewhere in this doc, no new work needed. Flagged the real exceptions: Supabase's EU→US region move must happen before real data exists (no in-place migration exists); and both conditional moves from today (Coolify→K8s, Celery→OpenFaaS/Knative) are real migration effort, not redeploys, if their triggers ever fire.
  • 2026-08-28: Added an "HA / CAP posture" section — governing pattern is CP at the data layer (Postgres transactions, correct for booking/payment integrity) and AP at the stateless app layer (horizontal redundancy). Audited every component: flagged Redis-as-Celery-broker as the one real gap worth closing given HA outranks low-ops in the stated priorities (single instance today, no Sentinel/replication); Postgres HA needs verification of actual plan-tier failover behavior, not new work; Meilisearch and the observability VPS are acceptable single points of failure for now.
  • 2026-08-28: Added a "Pre-launch checklist" — gates data safety (Supabase region/failover, pooling, tested backup-restore), availability (Redis broker HA, VPS sizing confirmed), observability (dashboards + alerting actually built, not just installed), payments (Stripe live-mode + webhook + Connect flow verified), security (TLS, auth flows, secrets hygiene), and rollout tooling (GrowthBook only if needed day one) — meant to be reviewed once before onboarding the first real users (even just 10), not needed during solo/free testing.
  • 2026-08-28: Added a "Phase definitions" section formalizing scale ranges: Phase 1 (launch, 10s–100s users, single VPS, budget-constrained solo operation), Phase 2 (growth, 1,000s–10,000s users, tiered VPS split, expected to be monetized enough to afford a hire or HA-grade managed upgrades while still solo-run day to day), Phase 3 (scaled steady-state — same foundation matured to automatic failover everywhere, no incident goes unnoticed). Referenced by these ranges (not calendar dates) throughout the doc. Made the Phase 1 HA posture explicit in the pre-launch checklist: 1 VPS at launch, no standby, accepted risk backed by a rehearsed (not just documented) manual failover procedure targeting a few-hours RTO — multi-VPS HA is explicitly deferred to Phase 2, not required to pass the launch gate.
  • 2026-08-28: Added explicit RTO targets per phase to the Phase definitions table — Phase 1: ~4 hours (manual, rehearsed runbook), Phase 2: ~15 minutes (semi-automatic, losing one web-tier box no longer causes downtime), Phase 3: seconds–low minutes (fully automatic, no single-box loss at any tier is user-visible). Filled in the Pre-launch checklist's RTO blank to match the Phase 1 figure. Added a Phase 2 launch checklist (mirrors the Pre-launch checklist's structure) as the second of two gates this doc now tracks — Phase 1 gate = first outside user, Phase 2 gate = the tiered-VPS trigger point — covering the tier-split rollout, closing out the Phase 1 accepted-risk items (Redis broker HA, Postgres HA re-verification) for real, the hire-vs-managed-upgrade decision implied by the Phase definitions, and expanded alerting. Purpose: make sure the money/hiring headroom expected at Phase 2 scale actually gets spent closing gaps, rather than reproducing the Phase 1 accepted-risk list indefinitely.
  • 2026-08-28: Added a "Data durability guarantee (Phase 2 onward)" section — decided: zero data loss becomes a hard requirement from Phase 2 on, covering both data at rest (DB) and data in flight (mid-processing at the moment of a crash). At-rest requirement: Supabase automatic-failover standby confirmed (not backups-only), PITR sized to a realistic detection window, restore rehearsed at real data volume. In-flight requirement: Redis broker HA elevated from "worth closing" to a hard Phase 2 gate; Celery tasks set acks_late/reject_on_worker_lost with every handler audited for idempotency (at-least-once delivery can duplicate-execute); a transactional outbox pattern introduced for any DB-write-then-async-follow-up flow (e.g. booking commit → Celery enqueue) to close the specific gap where a crash between commit and enqueue could silently drop work even though the DB write itself succeeded; Stripe webhook payloads persisted to Postgres before processing so they're replayable independent of Stripe's own retry; file uploads made atomic via a staging-key-then-DB-reference commit pattern. Threaded these into a new "Data durability" section of the Phase 2 launch checklist, and flagged the guarantee in the Phase 2/3 rows of the Phase definitions table.
  • 2026-08-28: Closed out the remaining infra/ops gaps flagged in the earlier "what are we missing" review. Added: On-call & incident response (founder is on-call through Phase 2, no formal rotation, lightweight runbook requirement); Staging / non-prod environment (decided: the Phase 1 single VPS gets repointed into staging once Phase 2's tier split happens, rather than adding new spend for a dedicated staging box); CI/CD and database migrations (Alembic for schema changes, GitHub Actions gating merges, migration dry-run against staging before prod); Access, secrets & audit (MFA required at Phase 1 launch on every account that can touch prod/money, admin_actions audit log table); Mobile app infrastructure (Expo push notifications, EAS OTA channels, /api/v1/ URL versioning to survive app-store review lag, Twilio extended to SMS); Domain, DNS & cross-provider risk (registrar continuity checklist; Hetzner/Supabase provider-level outage accepted as risk through Phase 2, same pattern as other documented accepted risks); Infra-as-code / reproducibility (export Coolify config, defer full Terraform/Ansible to Phase 2+); Identity verification & dispute handling (Stripe Identity for provider KYC, reusing the existing Stripe relationship; Stripe's native dispute flow for payment disputes, a Localz-side dispute center for Phase 2 per the existing roadmap doc); and Legal & compliance (ToS/Privacy Policy, retention/deletion, DPAs, marketplace tax reporting) flagged as required before the first outside user, with drafting explicitly out of this doc's scope. Threaded the new hard-gate items into both the Pre-launch and Phase 2 launch checklists. Also expanded Non-functional-Requirements.md from an 8-line placeholder into measurable targets tied to the Phase/RTO/durability decisions here, and added a concrete atomic-hold implementation note to Localz-Strategy-and-Architecture-Note.md §6 (the double-booking concurrency problem it already named but hadn't specified a mechanism for). Deliberately left Functional Requirements and Monetization untouched — both explicitly still in progress by the founder.
  • 2026-08-28: Reconciled Minimal-to-Production-Security-Implementation.md (the messaging/proxy-call/email-relay security build plan) against this doc's decisions — it predated them and had drifted: fixed its datastore from MongoDB to Postgres (matching the "Postgres-only in production" decision), replaced its "Swap-in plan for AWS later" table with the actual decided stack (Hetzner/Coolify, not an AWS migration), and added the admin-MFA/secrets-rotation/dependency-scanning/CI-CD coverage it was missing — cross-referenced back to this doc's canonical sections rather than duplicated. Explicitly noted booking-concurrency is out of that doc's scope (different subsystem) and pointed to where it actually lives (Localz-Strategy-and-Architecture-Note.md §6, Non-functional-Requirements.md §Correctness).
  • 2026-08-30: Recreated this entire file after discovering it (along with Cost-and-Value-Tracker.md, and the edits to Non-functional-Requirements.md, Competitors.md, Minimal-to-Production-Security-Implementation.md, Localz-Strategy-and-Architecture-Note.md, and README.md) had never actually persisted to disk — confirmed via git history showing zero trace and identical ~44-day-old mtimes across every file that was supposedly edited this session. Root cause unconfirmed (ruled out the 2-minute poll cron, which only pulls when the tree is already clean, so it can't have clobbered live edits) — most likely the writes never reached persistent disk in the first place rather than being overwritten afterward. Content restored verbatim from conversation history; committing immediately this time.

Next steps

  • Walk each open row above and lock a decision with a one-line rationale.
  • Once finalized, update Tech-Stack.md and Localz-Required-Stack.md to match, then remove this file.
Hermes Wiki