● Zion Boggan
repos/Stepping App/STEPPING_APP_IMPLEMENTATION_PLAN.md
zionboggan.com ↗
319 lines · markdown
History for this file →
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. |