A privacy-first community-run mutual-aid network for marginalized groups to share food, baby formula, and critical resources — without corporate or state surveillance.
Status: Phases 1–4 complete (2026-05-24). 172+ tests, real Supabase wiring, full resource marketplace, photo uploads with EXIF stripping, resource map (OSM + FSA aggregation), push notification infrastructure, error reporting, and Policy/ToS screens. See qa-reports/phase-2-closeout-2026-05-24.md and qa-reports/phase-3-4-security-sweep-2026-05-24.md for the full audit trail.
Everything below is shipped and in the codebase. Nothing here is "coming soon."
Sign in with email + OTP, then a three-step verification flow: enter your invite code → pick a privacy-safe handle (random adjective-noun-4digit, with a re-roll button) → wait for admin approval. Until you're approved, the Waiting Room screen holds you. Once approved, the gate automatically routes you to the marketplace. The handle picker soft-warns if your input looks like a real name — that's by design.
New users see a three-card tour explaining what the app does and doesn't collect, before they dive in. The tour respects reduced-motion settings (no animation when the OS flag is on). You can re-watch it anytime from your Profile.
Browse available resources by category (food, hygiene, baby, HRT, other). Filter chips live at the top of the feed. Your last-used filter is remembered between sessions.
When you see something you need, tap to claim it. Claiming is atomic — two people can't claim the same item at the same time (handled by a server-side transaction, not a client race). After pickup, either the poster or the claimant can mark it complete.
Fill in title, description, category, your postal prefix, and an optional photo. Photos are stripped of EXIF data on the device before upload, then stripped again server-side by an Edge Function — so no GPS coordinates, device model, or timestamp metadata leaves the app. The Edge Function uses imagemagick_deno for a full re-encode (not just a metadata scrub).
A neighborhood-level map showing where available resources are, using OpenStreetMap tiles. The map groups resources by Canadian FSA (the first three characters of a postal code — neighborhood-sized, not building-level). You see color-coded polygons: a few resources, several, or many. You never see an exact count or a pin at a specific address.
Screen readers get an equivalent text list below the map — every FSA that appears on the map also appears in the list with an accessible label.
Five values: food, hygiene, baby, HRT, other. The HRT category is discrete and intentional — it reflects the Keo persona's real need. It is not a subcategory of anything else.
After a resource is claimed, either party can mark it collected. The status moves from reserved to completed. Completed resources are pruned from the database after 30 days (by a nightly cron job).
Admins see a queue of accounts waiting for approval. Approvals and rejections go through server-side RPCs with an append-only audit log — no direct column writes, no way to delete the history.
The wiring is in place: opt-in per trigger (marketplace activity, admin approval result), default OFF for every user. Notifications are title-only on the lock screen — the body is always empty, so the resource name never appears where someone else could read it. The push token is not stored in AsyncStorage; it is re-read fresh each session.
This is infrastructure, not a fully deployed feature. The Edge Function for delivery (deliver_notification) and the server-side preference gate (migration 011) are queued for deployment — Sky applies them via the Supabase dashboard.
Opt-in crash and error reporting, default OFF. Before any error is sent, a PII heuristic strips email addresses, postal codes, handles, JWT-shaped strings, and Expo push token formats from the payload. Only then is the (truncated) error sent to a self-hosted Edge Function — no Sentry, no Bugsnag, no third-party analytics (per the community's privacy posture).
Plain-text policy screens built from typed constants in src/lib/policyText.ts. No WebView, no external URL, no risk of content injection.
Every component bakes in WCAG 2.5.5 (44pt minimum touch targets), accessibilityRole, accessibilityLabel, accessibilityHint, and accessibilityState. The map has a full-text screen-reader alternative. Animations skip entirely (not slow down — skip) when the OS reduced-motion flag is on.
| File / Directory | What it is |
|---|---|
PRD.md |
Sky's original product spec (some fields superseded by privacy redesign) |
CLAUDE.md |
Team context, gotchas, decisions log, file map, Role → Outputs map |
PRIVACY.md |
🟢 APPROVED. Jordan data model + Steve audit. Source of truth for data decisions. |
FEATURES.md |
Backlog ordered by value/cost |
DESIGN.md |
Visual system v1 with WCAG-verified contrast ratios |
LEARNINGS.md |
Durable patterns and gotchas — appended each phase |
CONTRIBUTING.md + SECURITY.md |
Contributor entry point + vulnerability disclosure policy |
community/ + research/ + designs/ |
Casey / Riley / Dani role homes |
qa-reports/ |
Audit reports, cycle briefings, privacy reviews for every phase |
supabase/schema.sql + supabase/migrations/ |
Full schema + 10 migration files. FILES ONLY — Sky applies via dashboard. |
supabase/functions/exif-strip/ |
Edge Function for server-side EXIF strip. Deploy via supabase functions deploy. |
src/lib/ |
All pure helpers + Supabase client + auth + push + error reporting + i18n |
src/components/ |
13 reusable UI primitives, all WCAG 2.5.5 + label compliant |
src/screens/ |
13 screens wired to real Supabase data |
src/navigation/ |
Bottom tabs + Home stack + Profile stack + deep-link auth gate |
npm install --legacy-peer-deps # required because of the React 19.1 pin
npm run typecheck # tsc --noEmit
npm test # 172+ tests across 13+ suites
npm run lint # eslint clean
npm run format # prettier auto-format
npm start # boots Expo dev serverThe app requires a Supabase project with the schema applied to show real data. Without it, the auth gate will fail to connect. See Setup below.
You need a Supabase project before the app does anything useful.
- Create a project at supabase.com.
- Run
supabase/schema.sqlin the SQL editor, then apply migrations001through010in order. - Set
config.sky_uuidto your Supabase user UUID, then runUPDATE public.users SET is_admin = true WHERE id = '<your-uuid>'via the service role. - Generate a first invite token:
INSERT INTO public.invite_tokens (token_hash, created_by) VALUES (crypt('<your-token>', gen_salt('bf', 10)), '<your-uuid>'). - Copy your project URL and anon key into
.env:
EXPO_PUBLIC_SUPABASE_URL=...
EXPO_PUBLIC_SUPABASE_ANON_KEY=...
Numbered apply steps with exact SQL for each migration live in qa-reports/cycle-1-auth-gate-2026-05-23.md.
A few pieces are files in the repo but not yet live on any Supabase project. Sky applies these via the dashboard:
- Migrations
011(push preference server gate) and012(push rate limit) — in their respectivesupabase/migrations/files. - EXIF strip Edge Function —
supabase functions deploy exif-strip+ wire the Storage webhook (steps insupabase/functions/exif-strip/README.md). deliver_notificationEdge Function — inrory/deliver-notification-edge-fn-2026-05-25branch.
See CLAUDE.md for stack details and full gotchas list.
The app ships a web build powered by Expo web + react-leaflet (for the resource map).
Live URL: https://mutual-mesh.vercel.app
Access: auth-gated — a valid Mutual Mesh account (invite token + Sky verification) is required. There is no guest mode. Jordan's web-gate advisory (2026-05-25) prohibits unauthenticated marketplace browsing.
Map: the web map uses react-leaflet + OpenStreetMap tiles via src/components/PlatformMapView.web.tsx. Metro's platform-specific file resolution serves this file instead of PlatformMapView.tsx (which imports react-native-maps) on web builds. Both files export the same PlatformMapView component and props type.
Running the web build locally:
npm run web # starts the Expo web dev serverThe Vercel deployment uses --legacy-peer-deps in installCommand (see vercel.json) because react-leaflet has a peer dependency conflict with the React 19.1 pin.