The Padel Society · Internal

Amity Exit — Engineering Handoff

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.

2026-08-24Prepared by Paquito for NestorAudience: Jeff · Kirk

01Where the code is — three branches, all pushed

The exit spans three repos. Each has a dedicated branch on GitHub as of today. Nothing pilot-related is running in production.

RepoBranchWhat's on it
padel-backendfeat/TPS-0-native-social-pilotMigration 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-padelfeat/TPS-0-native-social-pilotnative_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-pilotThe injection seam (22c29f3): a NativeSocialOverride hook on top of tps_main that lets the app swap the feed data source without forking further.
To build the mobile branch: check the fork out on 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.

02Why we're leaving Amity

03The decisions, in order

Decision 1

PoC against a 10× production dataset before any production code

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.

Decision 2

Feed is dual-write. Chat is not.

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.

Decision 3

Chat rollout is a big-bang cutover, not a percentage ramp

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.

Decision 4

PostHog is the only control plane; the DB only enforces

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.

Decision 5

Legacy Amity IDs are permanent, and media must be re-hosted

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.

Decision 6

The cord-cut is gated on min-version adoption, not a calendar week

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.

Decision 7

The existing code path is never made worse

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.

04What the PoC proved — and the bugs it caught

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.

BenchmarkTimeReplaces
Global feed, worst case (119 communities)2.0 msglobal-feeds + session mint + Upstash
Amity-compatible envelope (old binaries)3.1 mssame, for un-updated apps
Padel-native ranked feed9.1 msimpossible on Amity
Channel list + unread counts1.7 ms3 live collections + socket.io + an N+1
Message page, keyset over 200k rows0.57 msAmity message feed
Chat realtime end-to-end, on simulator22 msAmity socket path

Hard-won lessons encoded in the branch

05Why the app-side code was reverted this morning

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.

06What is not proven yet

07Suggested next steps

  1. Review the three branches per team rules (Lead + Security approval; the migration is additive-only and RLS-heavy — worth Security's full pass).
  2. Run the PoC locally: cd amity-exit/poc && ./scripts/run.sh — up, migrate, seed, compat gate, benchmarks, fully isolated from prod.
  3. Create the PostHog flag tps-XXX-native-social (multivariate: feed / feed-chat) once a ticket number exists.
  4. Needed from Nestor: a PostHog personal API key with project read access (for local_evaluation) + the project public API key. Nobody should go hunting for these.
  5. Phase-1 build order: extractor vs live Amity API → media rehost → offline outbox spike → then the cohort pilot behind the flag.