Everything decided, built, measured and reverted on the native-social pilot — so Jeff and Kirk can take the work over without archaeology. All three branches are pushed.
The exit spans three repos. Each has a dedicated branch on GitHub as of today. Nothing pilot-related is running in production.
| Repo | Branch | What's on it |
|---|---|---|
padel-backend | feat/TPS-0-native-social-pilot | Migration 20260824000000_native_social_pilot.sql (773 lines: tables, RLS, triggers, feed functions) + 3 Edge Functions: sync-social-cohort, mirror-amity-social, rehost-social-media. Additive-only — no ALTER/DROP on any existing prod table. |
mobile-app-padel | feat/TPS-0-native-social-pilot | native_social_repository.dart, amity_post_json_parser.dart, and the install() call in login_controller. Restored from the revert (ed527c48) onto its own branch. |
Amity fork (flutter_amity_uikit_beta_service) | wip/native-social-pilot | The injection seam (22c29f3): a NativeSocialOverride hook on top of tps_main that lets the app swap the feed data source without forking further. |
wip/native-social-pilot first — the app branch imports NativeSocialOverride from it. Day-to-day builds stay on tps_main exactly as the team rules say; the seam branch is only for pilot work.
entity_posts, joined_action_posts) round-tripped through Amity.tps_main) just to style and extend their UI. Every upstream change is a merge war.amity-exit/index.html in the repo folder.Isolated Supabase stack (amity-exit/poc/, own project id, own ports, additive-only guard that hard-fails on ALTER/DROP). 113k posts, 200k messages, 238k memberships. This caught real design errors before they shipped — see §04.
A post is a broadcast object — writing it to two stores is safe and idempotent. A message is a conversation — dual-writing it means a bidirectional bridge, echo loops, cross-clock ordering, phantom unreads and divergent deletes, all user-visible in the surface people use most. So: posts mirror to Postgres (non-fatal append, Amity stays canonical); chat gets closed native channels containing only cohort members, which never exist in Amity at all. A channel lives in exactly one store — that single rule deletes the bridge, the echo loop, the ordering problem and the phantom unreads. Full matrix: poc/PILOT-WRITE-MATRIX.md.
Because chat never dual-writes, per-user ramping would strand conversations between native and Amity users. At ~1,100 socially-active users the cutover is a short low-traffic freeze window: final extract, flip everyone. The feed lane is a normal percentage ramp — dual-write makes it safe. This corrects the original plan, which assumed both lanes ramped identically.
RLS can't call PostHog over HTTP, so social_cohort exists as a read-through cache of the flag — never a second admin surface. Kept in sync two ways: just-in-time on app launch (JWT in, server-side PostHog check — client tampering changes nothing) and a 15-minute reconcile cron so removals land without a deploy. Multivariate flag stages the lanes: feed vs feed-chat. Break-glass: delete from social_cohort kills the pilot instantly.
Every deeplink ever shared carries an Amity post id → legacy_amity_id survives forever and resolve_post() accepts both id forms. Historical images 404 the day the Amity contract ends → rehost-social-media copies them into Supabase Storage and rewrites URLs during extraction.
Old binaries keep talking to Amity forever. app_configs already carries force_update; Phase 5 fires when adoption crosses the threshold, not on "week 10". And removing amity_sdk changes native dependencies — Phase 5 is a full store release, not a Shorebird patch. Both code paths ship in the binary before any ramp; the flip itself is server-side.
Every Amity write in the app and backend today stays byte-for-byte as it is. Postgres writes are appended and wrapped non-fatally: if the whole native stack died, production behaviour would be unchanged for all ~17,200 users. Enforced by triggers, proven against a forced-membership attack in scripts/cohort-isolation-test.sql.
Warm timings on the 10× dataset. The compat gate (scripts/compat-checks.sql) is pass/fail and CI-ready: 9/9 assertions pass, including additive-only migrations, verbatim Amity ids, idempotent backfill, and Amity read-marker semantics.
| Benchmark | Time | Replaces |
|---|---|---|
| Global feed, worst case (119 communities) | 2.0 ms | global-feeds + session mint + Upstash |
| Amity-compatible envelope (old binaries) | 3.1 ms | same, for un-updated apps |
| Padel-native ranked feed | 9.1 ms | impossible on Amity |
| Channel list + unread counts | 1.7 ms | 3 live collections + socket.io + an N+1 |
| Message page, keyset over 200k rows | 0.57 ms | Amity message feed |
| Chat realtime end-to-end, on simulator | 22 ms | Amity socket path |
VACUUM ANALYZE after backfill is load-bearing.resolve_post() needed CASE, not AND — SQL doesn't short-circuit, so every legacy deeplink would have 500'd.SECURITY DEFINER helpers — self-referencing policies recurse (42P17), and RLS without GRANTs is authorisation theatre (42501).realtime.send()'s 4th arg is private (default true), not public. Server published public, client subscribed private, both reported success, zero messages delivered. Every SQL test passed while chat was completely broken — which is the whole argument for testing with a real client on the simulator.The pilot code was briefly on the perf branch and got reverted (ed527c48, 07:05 today) for one reason: it imported NativeSocialOverride from an unpushed Amity fork working tree, so the branch only built on Nestor's machine, and the pilot wasn't going to production inside a perf PR anyway. The three Edge Functions were also deleted from production at that point — prod is clean.
That blocker is now gone: the fork seam is pushed (wip/native-social-pilot) and the app code is restored on its own reviewable branch. Nothing about the design was wrong — the revert was about branch hygiene and buildability, not architecture.
cd amity-exit/poc && ./scripts/run.sh — up, migrate, seed, compat gate, benchmarks, fully isolated from prod.tps-XXX-native-social (multivariate: feed / feed-chat) once a ticket number exists.local_evaluation) + the project public API key. Nobody should go hunting for these.