| 1 | # Chicago Stepping App: Implementation Plan |
| 2 | |
| 3 | The Chicago Stepping App is a city-first community platform for **Chicago stepping**, the smooth, gliding, soulful 6-/8-count partner dance that originated on Chicago's West and South sides (sets, weekenders, classes, workshops, competitions, DJs, promoters/hosts, stepping groups). It ships web-first as an installable PWA, then native iOS/Android, with **live streaming of sets** as the headline feature. The build is one TypeScript spine (Next.js + tRPC + Zod + Drizzle over Supabase Postgres) with the hard, undifferentiated media work (transcode, adaptive delivery, low-latency live, auto-VOD) bought from a single managed provider behind a swappable interface. Everything users browse is scoped by `city`, anyone with an account can post (optimistic publish, soft moderation), and roles/verification/monetization are pre-modeled as dormant columns and pluggable services so they switch on later without a migration or rewrite. Launch cities are **Chicago (flagship), Atlanta, Detroit, Houston, Charlotte, and Washington-DC/DMV**, with Chicago seeded deepest as the home of the dance and the primary dogfood market. |
| 4 | |
| 5 | This is **Chicago stepping, not Divine Nine / Greek / NPHC stepping**. There are no fraternities, sororities, chapters, step shows, strolls, or Greek letters anywhere in the product. The core social object is a **"set"**: a partner-dance social gathering in a city. See `DADS_PLAN_SEQUENCED.md` and `Plan-Transcribe/transcripts/_ALL_TRANSCRIPTS.md` for dad's verbatim words and the A-Q feature list. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Recommended Stack |
| 10 | |
| 11 | One decisive stack. No optionality at the top level; optionality lives behind interfaces. |
| 12 | |
| 13 | | Layer | Choice | Why | |
| 14 | |---|---|---| |
| 15 | | **Web / PWA** | **Next.js 15 (App Router) + React + TypeScript + Tailwind + shadcn/ui**, installable PWA via `next-pwa` | Server-rendered discovery pages are Google-indexable (critical for "Chicago steppin' set 2026" / "Atlanta stepping this weekend" search), fast on mobile data, app-like on day one | |
| 16 | | **API** | **tRPC** colocated in the Next app, with **Zod** schemas in a shared package | End-to-end type safety into web and future native; no separate API service to run for MVP | |
| 17 | | **Async / queue** | **Inngest** (or Upstash QStash) | Serverless, scale-to-zero; handles webhooks fan-out, push, scheduled city-data refresh | |
| 18 | | **Database** | **Supabase Postgres + PostGIS**, **Drizzle ORM** | DB + Auth + Storage + RLS + real geo ("near venue") in one managed service; typed migrations from the shared package | |
| 19 | | **Auth** | **Supabase Auth** (email magic-link + Google + Apple OAuth) | Apple sign-in is mandatory for the future iOS build; JWTs flow straight into tRPC and RLS | |
| 20 | | **Image storage** | **Supabase Storage** (S3-compatible, behind CDN) | Flyers, avatars, profile/group photos; RLS-aware, cheap | |
| 21 | | **Video + Live (VOD, transcode, RTMP/SRT/WHIP ingest, LL-HLS, auto-record)** | **Mux** | See decision below | |
| 22 | | **Live chat / realtime** | **Supabase Realtime** | Already on Supabase; live chat, viewer counts, reaction bursts with no websocket server to run | |
| 23 | | **Text moderation** | **OpenAI Moderation API** (free) + slur/profanity denylist | Comments, captions, descriptions, chat | |
| 24 | | **Visual moderation** | **Hive Moderation** (or AWS Rekognition) | Per-asset nudity/violence/CSAM scoring in the webhook handler | |
| 25 | | **Maps / places** | **Google Maps Platform** (Places + Maps SDK), cached into Postgres | Geocoding, travel guide, "near venue"; cache aggressively to control spend | |
| 26 | | **Web hosting** | **Vercel** | First-party Next.js host: preview deploys, edge CDN, image optimization, zero-config | |
| 27 | | **Data plane** | **Supabase Cloud** | Don't self-host the DB for an always-on consumer app | |
| 28 | | **Homelab role** | Staging, scheduled **city-data ingest** job, backups, documented self-host fallback | Keep production user-facing infra in the cloud so it survives a homelab reboot | |
| 29 | | **Native (later)** | **Expo (React Native)** in a **Turborepo monorepo** sharing `packages/` | Share the logic, fork the shell, see Architecture | |
| 30 | |
| 31 | ### Decision: Mux for the media + live plane (resolving Mux vs Cloudflare Stream) |
| 32 | |
| 33 | The architecture contributor chose **Mux**; the media contributor leaned **Cloudflare Stream** on predictable per-minute pricing. **We pick Mux**, decisively, for three reasons: |
| 34 | |
| 35 | 1. **DX for a solo operator is the binding constraint, not the per-minute bill.** Pre-monetization volume is low; the cost delta between providers is single-digit dollars per set. Mux's API ergonomics (clean asset IDs, signed playback, `stream.active`/`asset.ready` webhooks, auto-VOD on stream end, storyboards/thumbnails) save real build-and-debug time, which is the scarce resource here. |
| 36 | 2. **One provider covers VOD + Live + auto-record + WHIP browser ingest + RTMP/SRT pro ingest**: the same single integration both contributors wanted, just landing on the better-DX vendor. |
| 37 | 3. **The cost concern is fully neutralized by the seam.** All media access goes through one internal `MediaService` interface. Mux today; Cloudflare Stream (cheaper at scale) or a homelab MediaMTX/FFmpeg fallback tomorrow is a one-adapter change, not an app change. We document Cloudflare as the at-scale swap target the moment delivery cost becomes the dominant line item (which lands after ticketing/ads revenue exists). |
| 38 | |
| 39 | Cost posture is unchanged either way: **zero idle cost**, usage-based, no always-on encoding fleet, perfect for a spiky weekender-night events calendar. |
| 40 | |
| 41 | --- |
| 42 | |
| 43 | ## Architecture |
| 44 | |
| 45 | **Shape:** a single TypeScript codebase that is web-first now and native-ready later, with managed cloud for everything user-facing and the homelab reserved for staging, scheduled jobs, and documented fallback. |
| 46 | |
| 47 | ``` |
| 48 | ┌─────────────────────────────────────────────┐ |
| 49 | Browser / PWA ───────▶│ Next.js 15 on Vercel (SSR discovery pages) │ |
| 50 | (Expo RN later) ─────▶│ + tRPC API routes + shared packages/ core │ |
| 51 | └───────┬───────────────┬───────────────┬──────┘ |
| 52 | │ Drizzle/RLS │ MediaService │ events |
| 53 | ▼ ▼ ▼ |
| 54 | ┌────────────────────┐ ┌───────────┐ ┌────────────┐ |
| 55 | │ Supabase Postgres │ │ Mux │ │ Inngest │ |
| 56 | │ +PostGIS +Auth │ │ Video+Live│ │ queue/cron │ |
| 57 | │ +Storage +Realtime │ │ (RTMP/SRT │ │ (webhooks, │ |
| 58 | └────────────────────┘ │ /WHIP → │ │ push, │ |
| 59 | ▲ │ LL-HLS, │ │ ingest) │ |
| 60 | │ │ auto-VOD) │ └─────┬──────┘ |
| 61 | ┌─────────┴────────┐ └─────┬─────┘ │ |
| 62 | │ Homelab │ │ │ |
| 63 | │ staging · city │◀── Google Places ───────┘ |
| 64 | │ ingest · backups │ (cached → Postgres) |
| 65 | │ video fallback │ |
| 66 | └──────────────────┘ |
| 67 | ``` |
| 68 | |
| 69 | ### Key architectural decisions |
| 70 | |
| 71 | - **Monorepo from day one (Turborepo).** `apps/web` (Next.js) ships now. `packages/` holds the API client, types, Zod schemas, Drizzle schema, business logic, and design tokens. `apps/mobile` (Expo) is added later and consumes the **same** `packages/` core. **We share the logic, not the literal UI**: no React Native Web / Solito / Flutter compromise, because the web app's SSR/SEO value is too high to sacrifice for pixel-for-pixel code share. Two view layers, one brain. |
| 72 | - **Read-heavy discovery surfaces are React Server Components** (sets/events, promoters, city guides, stepping-group/DJ profiles) so they are crawlable and fast. Interactive surfaces (post forms, live player + chat, search) are client components. |
| 73 | - **The public API stays serverless** so it scales to zero between sets and absorbs weekender-night spikes without capacity planning. Heavy/async work (Mux webhooks → asset state transitions, push fan-out, scheduled city-data refresh) runs through Inngest, promoted to a dedicated homelab worker only if volume ever demands it. |
| 74 | - **Live streaming is fully managed (Phase 4).** A broadcaster goes live via (a) **pro path:** an issued RTMP/SRT stream key into OBS / a venue encoder, or (b) **casual path:** a browser/phone **WHIP (WebRTC)** "Go Live" button so any attendee can stream without OBS. Mux transcodes both to a low-latency **LL-HLS** ladder (3-8s glass-to-glass, the right tier for watch-and-chat). Viewers play LL-HLS with **hls.js** (native HLS on Safari/iOS). On disconnect, Mux **auto-records to a VOD asset** that drops into the Videos feed through the normal moderation pipeline. Live chat runs over Supabase Realtime keyed to the stream id, not a hand-built websocket server. |
| 75 | - **Provider-agnostic seams** isolate every buy-vs-build vendor: `MediaService` (upload, asset, live, terminate, webhook, sign token) and `ModerationService` (checkText, checkAsset, recordReport, evaluateThresholds). Swapping Mux → Cloudflare/self-host, or one moderation vendor for another, is a single adapter. |
| 76 | - **Roles, moderation, and monetization are pre-wired, dormant.** RLS policies and `role`/`status`/`verifiedAt` columns exist now (allow-all today); Stripe and listing/ticketing tables slot into tRPC later. They are switches, not migrations. |
| 77 | |
| 78 | --- |
| 79 | |
| 80 | ## Data Model |
| 81 | |
| 82 | Matches the schema actually in `packages/db/src/schema.ts` and the unions in `packages/types`. Postgres (Supabase), Drizzle, PostGIS. We keep **explicit typed entities** (Event, Video, Promoter, SteppingGroup, etc.) rather than one generic `listing` blob, but every user-creatable entity shares two **mixins** so they behave uniformly. |
| 83 | |
| 84 | > Note on what was removed: Chicago stepping has no Greek / NPHC affiliation, so the old `Organization`, `Chapter`, `StepTeam`, and `ChapterMembership` tables (and `ORG_TYPE`) were **deleted**. What used to be `stepTeam(s)` is now **`steppingGroup(s)`**. There is no separate `Instructor` table; an instructor is a text field on `Class`. Live streaming ships in Phase 4, so a `LiveStream` entity is added then; in MVP, `Video` is embed-only. |
| 85 | |
| 86 | ### Conventions (every entity) |
| 87 | - **PK:** `id` UUID (default random). **Audit:** `createdAt`, `updatedAt` (timestamptz). |
| 88 | - **Ownership mixin** (every user-creatable entity): `ownerId` (FK User, nullable). Lets a fan stub a Promoter/DJ/SteppingGroup and the real party claim it later via ownership transfer. |
| 89 | - **Moderatable mixin** (every user-creatable entity): `status` enum(`active`,`pending`,`hidden`,`removed`) default `active` (optimistic publish), `verifiedAt` (nullable), `hiddenAt` (nullable). Soft-hide only; never hard delete. |
| 90 | - **City scoping:** `cityId` (FK City) on every browseable entity. This is the single axis the whole Discover experience filters on. |
| 91 | - **Geo:** venues and travel places carry `lat`, `lng` for "near venue" queries (PostGIS). |
| 92 | |
| 93 | ### Identity & accounts |
| 94 | - **User**: `handle` (unique), `displayName`, `avatarUrl`, `role` enum(`member`,`moderator`,`admin`) default `member`, `trustTier` int default 0 (drives rate limits + auto-publish vs queue). *(Mirrors Supabase `auth.users`; everyone is `member` in MVP, the columns exist so roles/verification drop in with zero migration.)* |
| 95 | |
| 96 | ### Geography (reference, drives all scoping) |
| 97 | - **City**: `name`, `slug` (unique, used in URLs: `/chicago/sets`), `metroLabel` (e.g. "DMV"), `state`, `lat`, `lng`, `tier` (launch ordering), `isLaunched` bool (gates public browsability). |
| 98 | - **Venue**: `cityId`, `name`, `address`, `lat`, `lng`. *Anchor for "near venue" travel queries.* |
| 99 | |
| 100 | ### Community (city-scoped, claimable): dad's A-Q vocabulary |
| 101 | - **SteppingGroup** (dad `[K]`): `cityId`, `name`, `bio`, `links`. The per-city crews/groups. *Moderatable.* |
| 102 | - **Promoter** (the hosts who throw sets): `cityId`, `name`, `bio`, `links`. *Moderatable.* |
| 103 | - **DJ** (dad `[C]`): `cityId`, `name`, `bio`, `links`. Per-city stepping DJs. *Moderatable.* |
| 104 | - **Class** (dad `[B]`): `cityId`, `title`, `instructor` (text), `schedule`, `venueId` (nullable), `priceText`, `isFree`. *Moderatable.* |
| 105 | |
| 106 | ### Events & sets |
| 107 | - **Event** (dad `[E]` sets, `[A]` promotions, `[F]` special events; central entity): `cityId`, `venueId` (nullable), `title`, `type` enum(`set`,`weekender`,`class`,`workshop`,`competition`,`special`,`social`) (default `set`), `description`, `startsAt`, `endsAt`, `externalUrl`, `ticketUrl` (dormant), `priceText`, `isFree`. *Moderatable.* A **"set"** is the core object: a partner-dance social gathering in a city. A **"weekender"** is an out-of-town multi-day stepping event. |
| 108 | |
| 109 | ### Media |
| 110 | - **Video** (dad `[D]`): `cityId`, `title`, `embedUrl`, `platform` enum(`youtube`,`instagram`,`tiktok`,`other`), `subjectType` + `subjectId` (polymorphic link back to what the video is about). *Moderatable.* **Embed-only in MVP** (YouTube/IG/TikTok); native upload/transcode arrives with the `MediaService` in Phase 4. |
| 111 | |
| 112 | ### Travel guide (per-city: dad's `[I]`,`[J]`,`[L]`,`[M]`,`[N]`,`[O]`) |
| 113 | - **TravelPlace**: `cityId`, `category` enum(`hotel`,`restaurant`,`mall`,`attraction`,`hospital`), `name`, `address`, `lat`, `lng`, `source` enum(`manual`,`google`), `placeRef` (external id, e.g. Google `place_id`). *Moderatable.* Same table holds curated and Places-pulled rows. Entertainment `[P]` and city main attraction `[J]` roll up under `attraction`. |
| 114 | |
| 115 | ### Cross-cutting: moderation & engagement |
| 116 | - **Comment** (polymorphic, dad `[H]`): `subjectType` enum(`event`,`video`,`promoter`,`dj`,`stepping_group`,`class`) + `subjectId`, `body`. *Moderatable.* |
| 117 | - **Flag**: `subjectType` + `subjectId` (any moderatable, incl. Comment), `reporterId`, `reason`, `status` enum(`open`,`reviewed`,`actioned`,`dismissed`). Distinct-reporter + reporter-trust weighting drives auto-hide thresholds. |
| 118 | - **AuditLog**: `actorId`, `action`, `subjectType`, `subjectId`, `meta` (jsonb). Every moderation action writes here (appeals + DMCA records). |
| 119 | |
| 120 | ### Joins |
| 121 | - **EventParticipant** (Event ↔ SteppingGroup): `eventId`, `steppingGroupId`. |
| 122 | - **EventDJ** (Event ↔ DJ): `eventId`, `djId`. |
| 123 | - **GroupMembership** (User ↔ SteppingGroup): `steppingGroupId`, `userId`, `roleInGroup`. |
| 124 | |
| 125 | ### Future-proofing hooks (defined, dormant in MVP) |
| 126 | - **Monetization-ready:** `ticketUrl`/`priceText`/`isFree` on Event and Class (external links now); future `Listing`/`Order`/`Subscription` tables attach to Promoter via `ownerId` in Phase 5. No paywall logic in MVP. |
| 127 | - **Live-ready (Phase 4):** a `LiveStream` entity (provider input, ingest, LL-HLS playback, auto-VOD) is added when live ships; the `MediaService` seam is stubbed now so it is an adapter, not a refactor. |
| 128 | - **Native-ready:** all entities API-first (UUID PKs, no web-only assumptions); media payloads carry provider/source IDs that serve web and native players identically. |
| 129 | |
| 130 | --- |
| 131 | |
| 132 | ## MVP Scope & Phased Roadmap |
| 133 | |
| 134 | Guiding rule, from dad: **the smallest thing that makes a traveling stepper or a local dancer open the app twice is a feed of sets and events they trust, organized by city.** Everything else hangs off that. The phases below follow dad's own build order from `DADS_PLAN_SEQUENCED.md` (his A-Q feature list). Live streaming is sequenced *last among features* despite being the headline, it's the most operationally heavy (cost, abuse, bandwidth) and must not gate the rest of the product. |
| 135 | |
| 136 | ### Phase 0: Re-base the project onto Chicago Stepping *(do before any feature work)* |
| 137 | 1. Re-theme all docs and scaffold copy to Chicago stepping; strip every Divine Nine / HBCU / NPHC / fraternity / "step show" / "stroll" / Greek-letter reference. |
| 138 | 2. Authoritative definition + scope copy uses the §0 correction (partner dance, born Chicago West/South sides, 1940s-60s, from the bop/jitterbug, 6-/8-count). |
| 139 | 3. Confirm the six launch cities for Chicago-stepping culture: **Chicago (flagship), Atlanta, Detroit, Houston, Charlotte, Washington-DC/DMV** (dad names Houston and Charlotte). |
| 140 | 4. Keep the existing stack/monorepo (Next.js + tRPC + Drizzle + Supabase + Mux): only the theme/content was wrong. |
| 141 | |
| 142 | ### Phase 1: The spine: Cities + content posting + discovery |
| 143 | **Goal:** ship dad's #1 goal, a city-organized feed of sets, events, promos, and videos that replaces hunting on YouTube/Facebook Live. |
| 144 | - **City model & directory**: the backbone; everything is city-scoped. City picker locked to the six launch cities. |
| 145 | - **Open posting + accounts** (dad: anyone can contribute; "somebody in Houston can send me the video"). |
| 146 | - **Sets `[E]`**: the core object, a set/event with city, venue, date/time, host. Event `type` defaults to `set`. |
| 147 | - **Events & promotions `[A]` `[F]`**: upcoming sets, weekenders, promotions, special/cause events. |
| 148 | - **Videos `[D]`**: per-city video, embed-only in MVP (YouTube/IG/TikTok) with title + city tag. |
| 149 | - **Comments `[H]`**: on sets/events/videos. |
| 150 | |
| 151 | ### Phase 2: The people & the music |
| 152 | **Goal:** make it the directory of who's who in each city. |
| 153 | - **Promoters / hosts**: profiles, their sets auto-listed (implied throughout; the people who throw sets). |
| 154 | - **Stepping groups `[K]`**: per-city crews/groups. |
| 155 | - **DJs `[C]`**: per-city stepping DJs (who's spinning, where). |
| 156 | - **Classes `[B]`**: per-city lessons (instructor, schedule, location). |
| 157 | |
| 158 | ### Phase 3: The traveling-stepper city guide |
| 159 | **Goal:** make it worth traveling with (dad, repeated: *"when you go out of town to certain cities"*). |
| 160 | - **Event venues / centers `[I]`** + **city main attractions `[J]`**. |
| 161 | - **Hotels `[N]`** (host + additional nearby). |
| 162 | - **Restaurants `[L]`**, **malls/shopping `[M]`**, **entertainment `[P]`** near the set/hotel. |
| 163 | - **Hospitals `[O]`**: nearest hospital to the venue (safety). |
| 164 | *(Curated/manual entries first; optional Google Places enrichment later, cached into `TravelPlace`.)* |
| 165 | |
| 166 | ### Phase 4: Live |
| 167 | **Goal:** deliver the headline. |
| 168 | - **Live `[G]`**: go live in-app for sets/weekenders, pro RTMP/SRT key path + casual WHIP browser path; LL-HLS playback; **auto-VOD** into the Videos feed. Live chat over Supabase Realtime. |
| 169 | *(Most technically involved; Mux Live. Native video upload/transcode via `MediaService` lands here too.)* |
| 170 | |
| 171 | ### Phase 5: Sustainability |
| 172 | **Goal:** fund upkeep. |
| 173 | - **Monetization `[Q]`**: design schema for it from day one (already dormant on Event/Class), switch it on here: ads and/or paid download and/or free; later promoter/featured listings, ticketing/RSVP, sponsorships. Goal is covering maintenance (frequent edits/updates by the dev office), per dad. Decide model with dad/Zion. |
| 174 | |
| 175 | **Gating summary:** Phase 0 is documentation/theme (and is already underway). Phase 1 is the only phase with hard schedule pressure (it ships dad's north star). Phases 2-3 are mostly data + admin work. Phase 4 is the single biggest engineering investment, correctly placed after product-market signal. Phase 5 is additive flags + payment rails, no re-architecture. |
| 176 | |
| 177 | --- |
| 178 | |
| 179 | ## Media, Live Streaming & Moderation |
| 180 | |
| 181 | ### Design principles |
| 182 | - **Buy the hard, undifferentiated parts** (transcode ladders, edge delivery, low-latency live) from Mux; spend Zion's time on the community product. |
| 183 | - **Provider-agnostic seam:** all media touches go through `MediaService`; all trust-and-safety through `ModerationService`. |
| 184 | - **Moderation is a property of the pipeline, not a bolt-on.** Every piece of user media flows through one state machine; later roles/verification just change *who* fast-paths through it. |
| 185 | - **Cost scales with usage, not ambition**: no always-on encode fleet. |
| 186 | |
| 187 | ### Stored video (Phase 4: upload → transcode → playback) |
| 188 | 1. Client → `POST /api/media/uploads` `{ kind, context }`. |
| 189 | 2. API authenticates, runs **pre-upload gating** (rate limit + trust check), creates `media_asset` in `pending_upload`, asks Mux for a **one-time direct upload URL**. Browser uploads straight to Mux: bytes never transit our backend. |
| 190 | 3. Mux transcodes async → webhook `video.asset.ready` to `POST /api/media/webhooks/provider`. We verify signature, move to `transcoded`, kick off the moderation pre-check. |
| 191 | 4. On pass → `published`, visible in feeds. |
| 192 | |
| 193 | **State machine (single source of truth for transcode + moderation):** |
| 194 | ``` |
| 195 | pending_upload → uploaded → transcoded → in_review → published |
| 196 | ├──→ rejected |
| 197 | └──→ taken_down (post-publish, reversible/logged) |
| 198 | ``` |
| 199 | **Playback:** LL-HLS / HLS over Mux CDN, `hls.js` on web (native on Safari/iOS). Use **signed playback tokens from day one** (free now; ready for gated/ticketed content). Auto poster + storyboard hover-scrub. Let Mux auto-generate the ladder (240→1080p, H.264); store vertical phone video as-is and let the player letterbox. **Only metadata + provider IDs live in Postgres.** |
| 200 | |
| 201 | ### Live streaming (Phase 4) |
| 202 | - **Provider: Mux Live** (same vendor/integration as VOD; one bill). RTMP(S)/SRT ingest + WHIP (WebRTC) browser ingest → LL-HLS out (3-8s) → auto-record to VOD. |
| 203 | - **Pro path:** issued RTMP/SRT stream key into OBS / venue encoder. |
| 204 | - **Casual path:** in-app "Go Live" → phone camera via WHIP → Mux down-transcodes to the same HLS ladder. This is the open-contribution path. |
| 205 | - **Flow:** `POST /api/live/streams` → trust/rate gate → Mux creates a Live Input → store `live_stream` (`status=idle`). Broadcaster connects → webhook → `live` → appears in "Live now" rail. Viewers play LL-HLS; chat over Supabase Realtime keyed to the stream id. Disconnect → `ended`; auto-VOD enters the **same** `transcoded → in_review → published` path (live content is **not** exempt from moderation: reviewed post-hoc, takedownable). |
| 206 | - **Live moderation reality:** you cannot pre-review a live broadcast. So: (a) gate *who can start* via trust/rate limits, (b) prominent report button → past a low distinct-reporter threshold, **auto-pause** the input pending review, (c) auto-record everything so there's always an artifact, (d) a `POST /api/live/streams/:id/terminate` kill-switch the operator can hit from any device. |
| 207 | |
| 208 | ### Minimum-viable trust & safety (makes "anyone can post" survivable) |
| 209 | - **Identity floor:** account required to post (email/Google/Apple); anonymous read OK, anonymous write never. Per-account **trust score** (rises with age + clean history + verified email/phone) gates rate limits and auto-publish-vs-queue: this is exactly where verification badges plug in later (verified = higher tier). |
| 210 | - **Rate limits (primary spam defense):** posts per account per hour/day scaled by trust tier (new accounts tight: ~3 videos/day, 20 comments/hr); per-IP/device caps; upload size/duration caps (~10 min) to bound transcode cost; exponential backoff on repeat trips. |
| 211 | - **Automated pre-filters:** **text** via OpenAI Moderation (free) + slur denylist; **visual** via Hive/Rekognition scoring each transcoded asset before it leaves `in_review`; spam/link heuristics + perceptual-hash dedup. |
| 212 | - **Community reporting:** report action on every video/comment/event/stream; distinct-reporter-weighted thresholds → auto-hide (soft takedown) / auto-pause live; false-reporters lose report weight. |
| 213 | - **Operator console:** one mobile-friendly **moderation queue** (auto-flagged + reported-by-weight + `in_review`); actions = approve / reject / take down / warn / suspend / ban / terminate live; every action writes **AuditLog**; takedown is a reversible state, not a hard delete. |
| 214 | - **Legal baseline (not optional even in MVP):** designated **DMCA** agent + takedown endpoint (set footage carries copyrighted music); **CSAM** hits are auto-block + preserve + report to NCMEC (legal obligation); ToS + Community Guidelines scoped to **Chicago stepping** so off-topic has a stated removal basis; age gate at registration. |
| 215 | - **Deferred-but-designed roles:** `trustTier` + `role` exist from day one (default `member`). Verification later = a manual/document-backed tier bump that grants auto-publish, a badge, and optional self-moderation of one's own event comments. No pipeline rework. |
| 216 | |
| 217 | ### Provider-agnostic interfaces |
| 218 | ``` |
| 219 | MediaService: createDirectUpload(ownerId, ctx) · getAsset(assetId) · |
| 220 | createLiveStream(ownerId, eventId) · terminateLiveStream(streamId) · |
| 221 | handleWebhook(payload, sig) · signPlaybackToken(assetId, ttl) |
| 222 | ModerationService: checkText(text) · checkAsset(assetId) · |
| 223 | recordReport(targetType, targetId, userId, reason) · evaluateThresholds(targetId) |
| 224 | ``` |
| 225 | Mux today; Cloudflare Stream / Bunny / self-hosted homelab MediaMTX+FFmpeg tomorrow = one new adapter, no feed/queue/console changes. |
| 226 | |
| 227 | --- |
| 228 | |
| 229 | ## UX & Key Screens |
| 230 | |
| 231 | ### Principles |
| 232 | - **City-first, not feed-first.** A persistent city picker in the top nav scopes everything below it. Stepping is hyper-local and set-driven. |
| 233 | - **Discovery over creation.** Home + city pages are browse surfaces; posting is one tap away but never in the way. |
| 234 | - **Live is a first-class destination**: a red LIVE badge in the nav + a hero slot on Home when a set is streaming in the selected city. |
| 235 | - **Open posting, role-ready.** Every post is attributed; `role`/`verified` render as badges now, gate features later. |
| 236 | - **PWA-shaped:** bottom tab bar on mobile, left rail on desktop, same routes so native wrappers reuse the shell. |
| 237 | |
| 238 | ### Primary navigation |
| 239 | - **Global top bar:** Logo · **City picker** · Search · LIVE badge (only when streaming in the selected city) · Profile/Post. |
| 240 | - **5 primary destinations** (bottom tabs / left rail): **Home** · **Sets/Events** · **Watch** (videos + live) · **City** (per-city hub) · **Post (+)**. Account lives behind the top-bar avatar. |
| 241 | |
| 242 | ### Screens |
| 243 | - **Home**: hero = a current LIVE stream *or* the next big set in the selected city; rails: Live now, This weekend in {City}, Trending clips, New sets, Featured promoters, Switch city. Defaults to Chicago, surfaces the picker if no city chosen. |
| 244 | - **Sets/Events List**: filter by city, date range, type (`set`,`weekender`,`class`,`workshop`,`competition`,`special`,`social`). Card: flyer, title, date/time, venue, host/promoter, free/price badge, Going count. |
| 245 | - **Event/Set Detail**: flyer, description, date/time, venue + map + directions, host/promoter link, lineup (stepping groups, DJs), ticket/RSVP (external now), "Watch live" CTA if streamed, comments, **"Plan your trip"** deep-link into the City Guide pre-filtered to that venue. |
| 246 | - **Watch Home**: tabs **Live** (current/scheduled streams + viewer counts) and **Videos** (grid of recaps/set footage/tutorials, filter by city + tag). |
| 247 | - **Live Stream**: player, title, host, viewer count, **live chat** rail, "going to next set" CTA, share. |
| 248 | - **Video Detail**: player, title, uploader (badge), city/tags, description, comments, related clips. |
| 249 | - **City Home**: city header + hero, quick tiles to sub-sections, "Happening in {City}" strip, map of set locations. |
| 250 | - **City sub-sections**: **Stepping Groups** (→ profile), **Classes & Workshops** (→ detail), **DJs** (→ profile), **Promoters** (→ profile), **Set Locations/Map**, and the **City Travel Guide** (Hotels · Restaurants · Malls · Attractions · Hospitals, each with distance-to-venue when arrived via an event, map pins). |
| 251 | - **Post (sheet)**: pick a type (Set/Event · Video · Live · Class · DJ · Promoter · Stepping Group · Guide tip); minimal type-specific form; city pre-filled from the active picker. |
| 252 | - **Profile / Search / Auth**: my posts, saved/going, follows, uploads/streams (role badge slot reserved); unified search across all entities; lightweight sign-up (email + Google + Apple). |
| 253 | |
| 254 | ### Routing |
| 255 | `/` · `/events` · `/events/:id` · `/watch` · `/watch/live/:id` · `/watch/video/:id` · `/city/:city` · `/city/:city/{groups|classes|djs|promoters|map|guide}` · `/post` · `/u/:user` · `/search`. City is a route param + persisted in the picker. |
| 256 | |
| 257 | ### Build notes |
| 258 | - **One reusable card component** (image, title, meta row, action) with typed variants covers events/sets, videos, profiles, guide entries. |
| 259 | - **Badge/role system is purely presentational at MVP**: reads optional `role`/`verified`; turning on moderation later = populating fields + adding the review queue, not restructuring screens. |
| 260 | - Travel guide ships **curated** (hand-seed the launch cities), Google Places as optional enrichment, kept steppin'-relevant by anchoring entries to set venues. |
| 261 | |
| 262 | --- |
| 263 | |
| 264 | ## City Launch Plan |
| 265 | |
| 266 | City rollout is data-entry-bound, not engineering-bound, `City.isLaunched` and `City.tier` gate visibility, so launching a city = seeding its rows and flipping a flag. |
| 267 | |
| 268 | | Wave | Cities | `tier` | Role | Seed checklist before flipping `is_launched` | |
| 269 | |---|---|---|---|---| |
| 270 | | **1** | **Chicago** (flagship), **Atlanta** | 1 | Primary launch markets | 5-10 real upcoming sets/events; venues geocoded; 3-5 promoters; known DJs/stepping groups; embedded video links | |
| 271 | | **2** | **Detroit**, **Washington-DC/DMV** | 2 | First expansion | Same seed bar + curated travel-guide rows (hotels/restaurants/malls/attractions/hospitals near key venues) | |
| 272 | | **3** | **Houston**, **Charlotte** | 3 | Dad-named markets | Same seed bar; both named directly by dad as traveling-stepper destinations | |
| 273 | |
| 274 | - **DMV grouping** is handled by the `metroLabel` string (no `Metro` table yet): DC = "DMV". |
| 275 | - **Per-city seeding is the launch gate, not code.** Each new city needs: real upcoming sets/events, geocoded venues, a handful of promoters/DJs/stepping groups, and (Phase 3+) curated travel-guide rows anchored to its main venues. |
| 276 | - **Scheduled city-data ingest** (Google Places → hotels/restaurants/malls/hospitals/attractions) runs as the homelab cron job, caching results into the `TravelPlace` table to control API spend; manual curation always overrides. |
| 277 | - **Chicago-first dogfooding:** Chicago is `tier 1` and the home of the dance; use it earliest and deepest to exercise open-posting and moderation with a real crowd before the other cities go fully public. |
| 278 | |
| 279 | --- |
| 280 | |
| 281 | ## Build Checklist (first sprint: ship the Phase 1 spine) |
| 282 | |
| 283 | Ordered, concrete. Goal of sprint 1: a deployed, installable PWA where you can post and browse sets/events in Chicago + Atlanta with comments and embed videos. |
| 284 | |
| 285 | 1. **Repo + monorepo scaffold.** `create-turbo`; `apps/web` (Next.js 15 App Router + TS + Tailwind + shadcn/ui); `packages/{db,api,types,config,ui}`. Add `next-pwa` (manifest + service worker). |
| 286 | 2. **Supabase project.** Provision Postgres + Auth + Storage; enable **PostGIS**. Wire env into Vercel + local `.env`. |
| 287 | 3. **Drizzle schema (in `packages/db`, matches `schema.ts`).** Implement the timestamps + Ownership + Moderatable mixins, then **City, User, Venue, SteppingGroup, Promoter, DJ, Class, Event, Video, Comment, Flag, TravelPlace, AuditLog** + the joins (**EventParticipant, EventDJ, GroupMembership**). Include the dormant `role`/`trustTier`/`status`/`verifiedAt` columns now. Event `type` enum = `set`/`weekender`/`class`/`workshop`/`competition`/`special`/`social`. Generate + run migrations. |
| 288 | 4. **RLS policies (allow-all today, role-shaped).** Authenticated users get write on their own rows; reads are public where `status='active'`. Policies reference `role`/`status` so tightening later is a policy edit. |
| 289 | 5. **Seed reference data.** Six **City** rows: Chicago (`isLaunched=true`, `tier=1`, flagship), Atlanta (`tier=1`), Detroit + DC/DMV (`tier=2`), Houston + Charlotte (`tier=3`), with `metroLabel` where applicable; a few real **Venues** geocoded. *(No org/chapter seed, Chicago stepping has no Greek affiliation.)* |
| 290 | 6. **Auth.** Supabase Auth with email magic-link + Google + Apple; session → tRPC context; lightweight sign-up screen. |
| 291 | 7. **tRPC layer (in `packages/api`, matches `root.ts`).** Routers: `city`, `event`, `promoter`, `dj`, **`steppingGroup`**, `class`, `video`, `comment`, `flag`. Zod input schemas shared. CRUD with optimistic publish (`status='active'` on create) + ownership stamping. |
| 292 | 8. **City picker + scoping.** Top-bar picker locked to the six launch cities; route param `/city/:city`; every list query filters `cityId` + `status='active'`. |
| 293 | 9. **Sets/Events surface.** SSR Events List (filter by type + date) and Event Detail (Google Maps deep-link, share link) as Server Components. Event type defaults to `set`. |
| 294 | 10. **Open posting forms.** Post sheet with type picker → minimal forms for Set/Event, Video (embed URL), Promoter/DJ/SteppingGroup/Class; city pre-filled. |
| 295 | 11. **Videos (embed-only).** Render YouTube/IG/TikTok embeds with title + city tag; Watch grid. |
| 296 | 12. **Comments.** Polymorphic comment component under Event + Video; create/list via tRPC. |
| 297 | 13. **Directories.** Flat list pages for Promoters, DJs, **Stepping Groups**, Classes (reuse the one card component, typed variants). |
| 298 | 14. **Minimal moderation.** Admin-only view of recent posts; soft-hide (`status='hidden'`) action; "report" → creates a Flag; AuditLog write on hide. |
| 299 | 15. **Stub the seams.** Define `MediaService` + `ModerationService` interfaces in `packages/api` (no Mux/Hive wiring yet) so Phase 4 is an adapter, not a refactor. |
| 300 | 16. **PWA polish + deploy.** Manifest, icons, offline shell, mobile-first responsive pass. Deploy `apps/web` to Vercel; homelab as staging. Smoke-test install-to-home-screen on a real phone. |
| 301 | |
| 302 | --- |
| 303 | |
| 304 | ## Risks & Open Decisions |
| 305 | |
| 306 | | Item | Status | Resolution / mitigation | |
| 307 | |---|---|---| |
| 308 | | **Mux per-minute cost at scale** | Accepted pre-monetization | Zero idle cost; `MediaService` seam → swap to Cloudflare Stream / homelab the moment delivery cost dominates (post-ticketing). | |
| 309 | | **Two view layers (Next web + Expo native)** | Decided tradeoff | Share `packages/` logic, fork the shell. Worth it for SSR/SEO on discovery pages, the way a city sets directory actually gets found. | |
| 310 | | **Open posting → spam/abuse with no human pre-review** | Designed-for | Identity floor + trust-tiered rate limits + free text moderation + per-asset visual moderation + report thresholds + a mobile operator queue. "Survivable, not perfect." | |
| 311 | | **Live moderation can't be pre-reviewed** | Designed-for (Phase 4) | Gate who can go live, report-threshold auto-pause, auto-record everything, operator kill-switch. | |
| 312 | | **Legal: DMCA (copyrighted music in set/stepping footage) + CSAM** | Must ship in MVP | Designated DMCA agent + takedown endpoint; CSAM = auto-block + preserve + NCMEC report; ToS/guidelines scoped to Chicago stepping + age gate at signup. | |
| 313 | | **City seeding is the real launch bottleneck** | Operational, not technical | Each city needs hand-seeded sets/venues/profiles before `isLaunched`; Chicago dogfoods the flow first and deepest. | |
| 314 | | **Generic listing vs typed entities** | Resolved → typed | Keep explicit typed entities for query clarity + relational integrity; the shared mixins deliver the "one content model" uniformity without a blob table. | |
| 315 | | **Monetization model undetermined** | Deferred, pre-wired | `ticketUrl`/`priceText`/`isFree` + `ownerId` exist; Stripe + Listing/Order/Subscription tables slot into tRPC in Phase 5. No paywall in MVP. | |
| 316 | | **Google Places API spend** | Controlled | Cache aggressively into `TravelPlace`; manual curation overrides; scheduled homelab ingest, not live per-request calls. | |
| 317 | | **Live chat scale (Comment vs dedicated table)** | Watch item | Start on Supabase Realtime + capped chat tail; split to a dedicated `LiveChatMessage` table only if volume demands. | |
| 318 | | **Apple sign-in dependency for future iOS** | Pre-empted | Apple OAuth enabled in Supabase Auth from day one so the later App Store build has no auth blocker. | |
| 319 | | **Production must survive homelab reboots** | Decided | All user-facing infra in cloud (Vercel + Supabase + Mux); homelab is staging, scheduled jobs, backups, and documented fallback only. | |