A roommate-first mobile app for college students (17+). Swipe-based discovery, shared-school gated messaging, and a secondary Explore feed β built on Expo/React Native and Supabase.
Room is not a dating app, not a housing marketplace, and not a global social network. It's a structured, school-bound, trust-forward way for students to find compatible roommates.
- What Room Does
- Tech Stack
- Project Structure
- Getting Started
- Environment Variables
- Database & Backend
- Core Features
- Trust & Safety
- Architecture Notes
- Testing
- Documentation
- FAQ
| Tab | Purpose |
|---|---|
| Discovery | Roommate-first swipe stack. Right = like, left = dismiss, up = super-like, double-tap = expand profile. |
| Explore | School-bound social browsing. Ranked, filtered, never global. |
| Likes | See who liked you and who you liked back. |
| Messages | Inbox + Message Requests, gated by shared-school + enforcement state. |
| Profile | Your info, photos, schools, preferences, verification, settings. |
Hard rules (enforced server-side, not just client-side):
- Users must be 17+ with a verified birthdate.
- Messaging requires at least one shared school between sender and recipient.
- Verified users (selfie-verified) route DMs to Inbox; unverified to Message Requests.
- Blocks fully hide a user from Discovery, Explore, Likes, and Messages.
- Users in "Looking for friends" or "Found roommate" mode are removed from the Discovery stack.
| Layer | Tool | Why |
|---|---|---|
| Framework | Expo SDK 52 (React Native 0.76, New Architecture enabled) | Cross-platform iOS-first, OTA updates, managed native modules. |
| Language | TypeScript 5.3 | Type safety across services, hooks, and components. |
| Routing | expo-router 4 (typed routes) | File-based routing in app/. |
| Styling | NativeWind 4 + Tailwind CSS 3.4 | Utility classes that compile to RN styles. |
| Animation | react-native-reanimated 3.16 + gesture-handler 2.20 | 60fps swipe stack, gesture-driven sheets. |
| Bottom sheets | @gorhom/bottom-sheet 5 | Composer, filters, overflow menus. |
| State | React Context + custom hooks (no Redux) | Lightweight; Supabase realtime is the source of truth. |
| Navigation | @react-navigation 7 (under expo-router) | Tab + stack navigators. |
| Images | expo-image, expo-image-picker, expo-image-manipulator | Cached image rendering + photo upload pipeline (pick β resize β upload). |
| Notifications | expo-notifications + expo-device | Push notification tokens registered server-side. |
| Misc | expo-haptics, expo-blur, expo-linear-gradient, expo-clipboard, react-native-confetti-cannon | UX polish. |
| Layer | Tool |
|---|---|
| Database | Postgres (via Supabase) with Row-Level Security on every table |
| Auth | Supabase Auth β phone OTP |
| Storage | Supabase Storage β photos, selfie verification artifacts |
| Realtime | Supabase Realtime β chat messages, reactions, read receipts |
| Server logic | Supabase Edge Functions (Deno) β currently send-push-notification; trust/safety logic lives in Postgres functions + RLS |
| Migrations | 59+ SQL migrations in supabase/migrations/ |
- Jest + @testing-library/react-native β unit/component tests (
__tests__/) - Maestro β mobile E2E flows (
maestro/flows + skill bindings) - Supabase CLI β local DB, migration generation, type generation
- expo lint β linting
Roommate/
βββ app/ # expo-router file-based routes
β βββ (auth)/ # Onboarding: welcome β phone β OTP β name β birthday β photos β school β preferences β bio
β βββ (tabs)/ # Main tabs: discovery (index), explore, likes, messages, profile
β βββ chat/ # Chat thread screens
β βββ profile/ # Full profile views
β βββ filters.tsx # Discovery/explore filter sheet
β βββ settings.tsx
βββ src/
β βββ components/
β β βββ chat/ # Message list, composer, reactions, GIF picker, reply UI
β β βββ discovery/ # Swipe card, stack, photo carousel, action buttons
β β βββ explore/ # Feed cards, profile expanded view
β β βββ likes/ # Likes grid, footer, ad slots
β β βββ match/ # Match modal, confetti
β β βββ onboarding/ # Step UIs for the (auth) flow
β β βββ profile/ # Profile sections, photo manager, edit forms
β β βββ safety/ # Block, report, enforcement banners
β β βββ settings/
β β βββ verification/ # Selfie verification UI
β β βββ shared/ # Cross-feature primitives
β β βββ ui/ # Buttons, inputs, sheets, tokens
β βββ hooks/ # use-auth, use-discovery-stack, use-chat-messages, use-likes, use-explore-feed, use-enforcement, etc.
β βββ services/ # One module per domain β calls Supabase, returns typed data
β β βββ auth-service.ts, account-service.ts
β β βββ discovery-service.ts, explore-service.ts, filter-service.ts
β β βββ thread-service.ts, message-service.ts, gif-service.ts
β β βββ likes-service.ts, match-service.ts
β β βββ profile-service.ts, photo-service.ts, school-service.ts
β β βββ block-service.ts, report-service.ts, enforcement-service.ts
β β βββ selfie-service.ts, notification-service.ts, push-token-service.ts
β β βββ thread-preview.ts
β βββ stores/ # Lightweight client state
β βββ contexts/ # Auth context, theme, etc.
β βββ lib/ # Supabase client, helpers, constants
β βββ constants/
β βββ types/ # Generated Supabase types + app types
βββ supabase/
β βββ migrations/ # 58+ ordered SQL migrations
β βββ functions/ # Edge functions (send-push-notification)
β βββ seed.sql
βββ docs/
β βββ PRD.md # Product requirements (source of truth)
β βββ ARCHITECTURE.md
β βββ DB_SCHEMA.md
β βββ DECISIONS.md
β βββ EDGE_CASES.md, EDGE_CASE_FIXES.md
β βββ TRUST_AND_SAFETY.md
β βββ UI_UX_SPEC.md
βββ __tests__/ # Jest tests + setup
βββ qa/ # Manual QA scripts and notes
βββ assets/ # Icons, splash, fonts
βββ CLAUDE.md # Engineering execution guide for AI agents
βββ AGENTS.md
βββ app.json # Expo config
βββ tailwind.config.js, global.css, nativewind-env.d.ts
βββ tsconfig.json
- Node.js 20+ and npm
- Xcode 15+ (for iOS) or Android Studio (for Android)
- Expo CLI is invoked via
npx; no global install required - Supabase project β either a hosted project or local Supabase via the Supabase CLI
git clone https://github.com/yxf9tv/Roommate.git
cd Roommate
npm installCreate a .env file (or set values in app.json extra) with your Supabase credentials. See Environment Variables.
# Metro + dev menu
npm run start
# Native builds
npm run ios # requires Xcode
npm run android # requires Android Studio
# Web (limited β primary target is iOS/Android)
npm run webnpm test # Jest watch mode
npm run lint # expo lintRoom reads Supabase config from expo-constants. The client expects:
| Var | Purpose |
|---|---|
EXPO_PUBLIC_SUPABASE_URL |
Your Supabase project URL |
EXPO_PUBLIC_SUPABASE_ANON_KEY |
Public anon key (RLS enforced) |
Server-only secrets (used by Edge Functions and never shipped to the client):
| Var | Purpose |
|---|---|
SUPABASE_SERVICE_ROLE_KEY |
Privileged server key for Edge Functions |
EXPO_ACCESS_TOKEN |
Used by send-push-notification to deliver via Expo Push API |
Never commit
.env. Never trust the client for messaging eligibility, visibility rules, or enforcement state.
The schema lives in supabase/migrations/ β 59+ ordered SQL migrations, applied in numeric order (00001_create_enums.sql β 00059_unique_demo_cover_photos.sql).
npx supabase start # local Postgres + Studio
npx supabase db reset # apply all migrations from scratch
npx supabase db push # push to linked remote projectnpx supabase gen types typescript --linked > src/types/database.types.tsnpx supabase functions deploy send-push-notificationServer-side guarantees enforced in DB (RLS + RPC):
- Shared-school messaging gate
- Enforcement state checks (warning / 48h ban / 7-day suspension / permanent)
- Block visibility (hidden from Discovery, Explore, Likes, Messages)
- Ads gating thresholds (β₯10 swipes or first message sent)
- Ranking weights for Explore (30/25/20/15/10 split β see PRD Β§5)
- Card stack with photo carousel β swipe left/right, swipe up = super-like, double-tap = expand full profile
- Last photo loops back to first
- Mode-aware: users in "Looking for friends" or "Found roommate" are excluded from the stack
- Filters: age range, school, distance, lifestyle dealbreakers (soft + hard)
- Ad slot every ~10 cards, gated by engagement
- Only shows users sharing at least one school
- Weighted ranking: profile completeness (30%), recent activity (25%), verification (20%), engagement quality (15%), freshness (10%)
- Ranking weights are tunable via config β never pure popularity
- Tap any card to open the same full-profile view used in Discovery
- Who liked you / who you liked
- Ad placement in footer only β never blocking the action
- Inbox vs Message Requests routing based on selfie verification status
- Realtime delivery, typing indicators, read receipts
- Reactions, reply threading, edit, unsend
- Photo + GIF attachments (GIF search via Tenor/Giphy adapter in
gif-service.ts) - Tap header avatar to open the other user's profile
- Unread-thread badge on the Messages tab
Multi-step funnel under app/(auth)/: welcome β signup β phone β OTP β name β birthday β gender β school β photos β preferences β nitty-gritty β bio β login. Each step gates the next; partial accounts can't appear in Discovery/Explore.
- Reporting reasons: Harassment, Sexual content, Hate speech, Spam, Impersonation, Underage, Safety threat, Other
- Enforcement escalation: warning β 48h DM ban β 7-day suspension β permanent ban
- Block fully hides the blocker and blocked from each other across all surfaces
- Selfie verification optional but boosts ranking + routes DMs to Inbox
- Server is source of truth. The client never enforces messaging eligibility, visibility, or moderation. RLS + RPC functions in Postgres do.
- Services layer (
src/services/) wraps every Supabase call. Components and hooks never import the Supabase client directly β they go through a service. - Hooks (
src/hooks/) orchestrate services + realtime subscriptions and expose data to components. - No Redux. Local component state + React Context for cross-cutting concerns (auth, theme). Supabase Realtime drives live data.
- Immutable patterns. New objects, never mutate. Helps with React render correctness and concurrency safety.
- Small, focused files. ~200β400 lines typical. Components/services organized by feature domain.
- Unit/component: Jest +
@testing-library/react-native. Runnpm test. - E2E: Maestro flows. The
/maestroskill scaffolds and runs flows against the iOS Simulator or Android emulator. - Coverage target: 80%+ across unit, integration, and critical-path E2E.
The canonical specs live in docs/. Read these before making non-trivial changes:
docs/PRD.mdβ product behavior; the PRD overrides any other instructiondocs/ARCHITECTURE.mdβ services and server-side guaranteesdocs/DB_SCHEMA.mdβ table-by-table schemadocs/DECISIONS.mdβ recorded architectural decisionsdocs/TRUST_AND_SAFETY.mdβ moderation modeldocs/EDGE_CASES.mdβ edge cases and how they're handleddocs/UI_UX_SPEC.mdβ visual + interaction specCLAUDE.mdβ execution guide for AI agents working in this repo
Why is messaging gated by shared school? Trust. Room is not a global open network. Restricting messaging to people who share a school keeps interactions accountable and contextual. The gate is enforced server-side via RLS β clients can't bypass it.
Why phone OTP instead of email? Phone numbers are harder to mass-create than email addresses, which raises the cost of spam/abuse signup. Verified phone is also a prerequisite for several enforcement actions.
Why are there two browsing tabs (Discovery + Explore)? Discovery is the primary roommate-matching surface β narrow filters, swipe mechanics, mode-aware. Explore is a softer, school-bound social browsing layer for students who want to meet people but aren't actively roommate-hunting.
How does selfie verification change the experience? Verified users get a badge, route their first-DMs to the recipient's Inbox (not Message Requests), and get a ranking boost in Explore. It's optional.
What happens when a user picks "Found roommate" or "Looking for friends"? They're removed from the Discovery stack so other users don't waste swipes on someone who's done. They can still appear in Explore (Friends mode) and message existing threads.
Why React Native + Expo instead of native iOS/Android? iOS-first launch with Android parity. Expo SDK 52 with the New Architecture gives near-native performance on the swipe stack (Reanimated + Gesture Handler) while keeping a single codebase, OTA updates, and a fast dev loop.
Why Supabase instead of a custom backend? Postgres + RLS gives us strong server-side guarantees with very little glue code. Auth, Storage, Realtime, and Edge Functions are first-class. The trust/safety logic that needs to live server-side fits cleanly into RLS policies + RPC functions.
Where does push notification logic live?
The client registers a device token via push-token-service.ts. The Edge Function send-push-notification (Deno) calls the Expo Push API on relevant DB events.
How do I add a new migration?
npx supabase migration new <name>
# edit the generated SQL
npx supabase db reset # locally
npx supabase db push # remoteThen regenerate types (supabase gen types typescript --linked).
Is there an admin/moderation UI? Not in this repo. Moderation actions today are SQL/RPC-driven. A separate admin surface is planned.
Private. All rights reserved.