CarApp is a React Native (Expo) mobile app — a two-sided marketplace connecting vehicle owners with vetted, independent mobile car detailers and mechanics in the Northern Virginia / DC Metro area. Customers book services, track providers live, and pay via Stripe. Providers manage schedules, earnings, and reputation. Payments use Stripe Connect with a deposit model (15% at booking, remainder on completion).
- Framework: Expo / React Native
- Routing: Expo Router (file-based, like Next.js)
- Backend: Supabase (PostgreSQL + Auth + Realtime + Storage + Edge Functions)
- Caching / Ephemeral Data: Redis (live GPS caching, rate limiting, short-lived tokens)
- Language: TypeScript (strict mode)
- Payments: Stripe Connect
- Push Notifications: Firebase Cloud Messaging (FCM)
- Maps: Google Maps SDK + react-native-maps
- Analytics: Mixpanel
- Error Monitoring: Sentry
- Auth Storage: Expo Secure Store
- SMS Provider: Twilio (configured at Supabase project level for phone OTP — not in app code)
- Styling: NativeWind + Tailwind CSS
CarApp/ # Git repo root
├── Blueprint/ # Schema, policies, build plan docs
├── ARCHITECTURE.md
├── CLAUDE.md
├── .claudeignore
└── carApp/ # Expo app root
├── app/ # Expo Router screens (file = route)
│ ├── _layout.tsx # Root layout — auth gate
│ ├── (auth)/ # Unauthenticated screens (sign-in, otp-entry, otp-verify, pending-approval)
│ └── (tabs)/ # Authenticated tab screens
├── src/
│ ├── lib/
│ │ ├── supabase/ # client.ts, auth.ts, queries.ts, mutations.ts, storage.ts
│ │ ├── redis/ # GPS caching, rate limiting, short-lived tokens
│ │ ├── stripe/ # Stripe Connect integration
│ │ ├── checkr/ # Background check webhook handling
│ │ ├── persona/ # Identity verification flow
│ │ ├── notifications/ # Firebase Cloud Messaging (push.ts)
│ │ └── location/ # GPS utilities
│ ├── state/ # Zustand global state slices
│ ├── types/ # models.ts, supabase.ts (generated), navigation.ts
│ ├── utils/ # validators.ts, money.ts, date.ts
│ ├── components/ # Reusable UI components (domain-organized)
│ └── design/ # theme.ts, tokens.ts, typography.ts
├── supabase/
│ └── functions/ # Edge Functions (Deno runtime — not Node)
│ ├── stripe-webhook/
│ ├── checkr-webhook/
│ ├── persona-webhook/
│ ├── notify-booking-confirmed/
│ ├── notify-provider-enroute/
│ ├── notify-job-complete/
│ ├── notify-payout-processed/
│ ├── notify-kudos-received/
│ └── lug-ai/
├── e2e/ # Maestro E2E flows
└── assets/ # Fonts, images, icons
All secrets are stored in environment variables. Never hardcode any of these values in code.
Mobile app — stored in .env.local, prefixed with EXPO_PUBLIC_:
EXPO_PUBLIC_SUPABASE_URL
EXPO_PUBLIC_SUPABASE_KEY
EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY
EXPO_PUBLIC_GOOGLE_MAPS_API_KEY
EXPO_PUBLIC_SENTRY_DSN
EXPO_PUBLIC_MIXPANEL_TOKEN
EXPO_PUBLIC_FIREBASE_API_KEY
EXPO_PUBLIC_FIREBASE_PROJECT_ID
EXPO_PUBLIC_FIREBASE_APP_ID
Supabase Edge Functions — set via supabase secrets set, never in code:
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
CHECKR_API_KEY
CHECKR_WEBHOOK_SECRET
PERSONA_API_KEY
PERSONA_WEBHOOK_SECRET
ANTHROPIC_API_KEY
REDIS_URL
An .env.example file lives at the project root with all keys listed but no values — reference this when setting up a new environment.
- ALL database reads go in
src/lib/supabase/queries.ts. ALL writes go inmutations.ts. Never callsupabase.from(...)directly in a component or screen. - All new files use TypeScript with explicit types. Never use
any. - Never edit
src/types/supabase.tsmanually — it is auto-generated. - Never use
service_rolekey in client code — it bypasses RLS. - After any schema change run:
supabase gen types typescript --project-id <id> > src/types/supabase.ts
- Screen-level components live under
app/. Reusable UI lives undersrc/components/. - Never install a new package without checking
Blueprint/dependencies_listfirst.
- Global state (auth session, search filters, booking draft, sign-up draft, provider draft) uses Zustand. Slices live in
src/state/, one file per domain. - Localized state scoped to a single feature tree (multi-step forms, modals, a single screen) uses React Context.
- Never use Redux or any state library other than Zustand and React Context. Never use React Context for app-wide state.
- Money stored as integers (cents) in DB. Always format for display using
src/utils/money.ts. - Dates stored as ISO strings. Always parse/format using
src/utils/date.ts.
- All design values are defined in
src/design/tokens.ts— never hardcode hex values, font sizes, or spacing in components. - Use Inter for UI/body text, Space Grotesk for brand/display, JetBrains Mono for prices and booking IDs. No other fonts.
- All interactive elements must meet WCAG 2.1 AA — 44x44pt touch target minimum.
- All components must support dark mode via dynamic color tokens.
- Never store secrets in code — use
EXPO_PUBLIC_env vars for the app,supabase secrets setfor Edge Functions. - Never write directly to
provider_location_cachefrom the app — live GPS position goes to Redis; Postgres persistence is handled server-side.
- All async Supabase calls in
queries.tsandmutations.tsmust be wrapped in try/catch and return typed{ data, error }tuples. Never throw raw errors from the data layer. - UI error display pattern:
- Transient errors (network blip, retry succeeded): toast notification
- Form validation errors: inline below the relevant field
- Critical errors (auth failure, payment failure): full-screen error state with recovery action
- Every list screen must handle three states explicitly: loading, empty, and error.
- Never use
console.login production code — use Sentry breadcrumbs for context at key state transitions. - Use React Error Boundaries at the tab level to prevent a single screen crash from taking down the whole app.
Edge Functions run on Deno, not Node.js. This matters for imports, syntax, and available APIs.
- All Edge Functions live in
supabase/functions/<function-name>/index.ts - Use Deno import syntax:
import { serve } from 'https://deno.land/std/http/server.ts' - Never use
require()or CommonJS syntax inside Edge Functions - Secrets are accessed via
Deno.env.get('SECRET_NAME')— never hardcoded - Set secrets with:
supabase secrets set SECRET_NAME=value - Deploy a function with:
supabase functions deploy <function-name> - Test locally with:
supabase functions serve <function-name>
Planned Edge Functions:
| Function | Trigger | Purpose |
|---|---|---|
stripe-webhook |
Stripe event | Payment succeeded, payout processed |
checkr-webhook |
Checkr event | Background check status update |
persona-webhook |
Persona event | Identity verification status update |
notify-booking-confirmed |
DB insert on bookings | Push to customer + provider |
notify-provider-enroute |
Booking status → en_route | Push to customer |
notify-job-complete |
Booking status → completed | Push to customer |
notify-payout-processed |
Payout status → paid | Push to provider |
notify-kudos-received |
Kudos insert | Push to provider |
lug-ai |
App request | Anthropic Claude API proxy with system prompt |
- Use Supabase Realtime for:
messages(new message in active thread),bookings(status transitions during active booking) - Do NOT use Supabase Realtime for GPS position updates — live GPS is handled via Redis; read
provider_location_cachevia polling during active bookings - Always subscribe on component mount and unsubscribe on unmount via the
useEffectreturn function - Channel naming convention:
booking:{bookingId},thread:{threadId}
// Pattern — always clean up
useEffect(() => {
const channel = supabase
.channel(`booking:${bookingId}`)
.on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'bookings' }, handler)
.subscribe()
return () => { supabase.removeChannel(channel) }
}, [bookingId])- All route params must be typed in
src/types/navigation.ts - Push notification deep links must route to the relevant screen:
- Booking confirmed →
/(tabs)/bookings/[id] - Provider en route →
/(tabs)/bookings/tracking/[bookingId] - Rate now →
/(tabs)/bookings/[id](review sheet open) - Kudos received →
/(tabs)/more/provider - New message →
/(tabs)/inbox/[threadId]
- Booking confirmed →
- Multi-step flows (onboarding, booking, vetting) use stack navigation within a group — not modals
- Modals are reserved for confirmations, sheets, and alerts only
All file operations go through src/lib/supabase/storage.ts. Never call Supabase Storage directly from a component.
Storage buckets:
| Bucket | Contents | Access |
|---|---|---|
avatars |
User and provider profile photos | Public read |
booking-photos |
Before/after job photos | Booking participants only |
vetting-documents |
Identity docs, insurance, credentials | Private — service role only |
Rules:
- Compress images client-side before upload: max 1920px on longest side, 80% quality
- Accepted types:
image/jpeg,image/png,image/webp - Max file size: 10MB per file
- Construct public URLs using
supabase.storage.from(bucket).getPublicUrl(path)— never hardcode storage URLs
- Add Sentry breadcrumbs at key state transitions: auth, booking status changes, payment events, GPS start/stop
- Key Mixpanel events to instrument:
| Event | Trigger |
|---|---|
search_initiated |
User submits location search |
provider_viewed |
User opens provider profile |
booking_started |
User enters booking flow |
booking_confirmed |
Deposit payment succeeded |
booking_completed |
Job status → completed |
rating_submitted |
Gear rating + kudos saved |
lug_opened |
Lug AI bubble tapped |
provider_onboarding_started |
Provider opts into provider mode |
provider_approved |
verification_status → approved |
Offline resilience is deferred to post-MVP. Do not implement offline queuing, optimistic UI, or local caching unless explicitly instructed.
For now, the expected behavior on network failure is:
- Show an appropriate error state (see Error Handling section)
- Provide a retry action where possible
- GPS tracking: if connection drops during active booking, buffer the last known position and resume on reconnect — do not crash or clear the map
- Auth gate:
app/_layout.tsxlistens toonAuthStateChange→ routes to(auth)/sign-inor(tabs)/ - OTP auth: Email and phone OTP are supported via Supabase Auth's built-in OTP API (
signInWithOtp). No custom token table. Phone OTP requires Twilio configured in Supabase dashboard. - Role model: All users default to Customer. Provider mode is opt-in post-signup and requires full vetting completion before first booking.
- Service snapshots: Services are snapshotted as JSONB at booking time — price/name changes by providers never alter existing bookings.
- Content moderation: ALL outbound messages must pass through
containsFlaggedContent()invalidators.tsbefore insert; flagged body is replaced with[Message flagged for review]. - Deposit model: 15% of booking total collected at booking via Stripe; remainder captured on job completion;
deposit_forfeited = trueon late cancellations (within 24 hours). - Platform fees: Provider platform fee is 5% (0% for Founding Providers for first 3 months). Customer service fee is 2% added at checkout.
- Provider vetting: A provider must pass all 6 vetting steps (identity, background check, insurance, credentials, bank account, profile completeness ≥ 80%) before
verification_statusis set toapproved. - Live GPS: Provider location updates every 5 seconds during active bookings. Live position is cached in Redis. Last-known position is persisted to
provider_location_cachein Postgres. - Kudos: Kudos badges are distinct from gear ratings — they live in the
kudostable and are freeform positive badges ('Meticulous', 'Reliable', 'Magic Hands', 'Great Value', 'Fast Worker', 'Communicator'). - Gear ratings: Customers rate on 4 dimensions — Quality, Timeliness, Communication, Value (1–5 each). Overall score is a weighted composite.
- Dispute window: 48 hours post-service for either party to flag a rating for admin review.
- RLS: Every table has Row Level Security enabled. Always verify queries work under the correct Supabase auth role.
- Lug AI: Lug is powered by the Anthropic Claude API via the
lug-aiEdge Function. Responses must be constrained by a system prompt referencing the CarApp service catalog. Always provide a human escalation path. - In-app communications only: No personal phone numbers, email addresses, or external payment handles may be shared in messages. Auto-detection of patterns like 'Venmo me', phone numbers, or email addresses triggers flagging.
All values are defined in src/design/tokens.ts.
| Role | Hex |
|---|---|
| Deep Indigo (primary brand, headers, active states) | #3D3B8E |
| Electric Blue (CTAs, links, interactive highlights) | #1A6DFF |
| Emerald Green (success states, completed bookings) | #10A96A |
| Gear Gold (gear rating icons, premium badge, Lug accent) | #D4A017 |
| Off-White (app background, card surfaces) | #F7F8FC |
| Charcoal (body text, primary content) | #222222 |
| Mid Gray (secondary text, placeholders, inactive) | #777777 |
| Role | Font |
|---|---|
| Brand / Display | Space Grotesk |
| UI / Body | Inter |
| Monospace (prices, booking IDs) | JetBrains Mono |
- Border radius: 12px (cards), 8px (inputs), 24px (primary action buttons)
- Icons: Lucide Icons as base library
- Motion: spring-based animations via React Native Reanimated — no linear tweens in primary UI
After every code change, write the appropriate tests before considering the task complete.
| What changed | Test type | Tool | Location |
|---|---|---|---|
| Utility functions, state stores | Unit | Jest | __tests__/ adjacent to file |
| Supabase queries / mutations | Integration | Jest + mocked Supabase client | __tests__/ adjacent to file |
| Complete user flows | E2E | Maestro | e2e/ at project root |
main— production only. Never commit directly.dev— integration branch. All features merge here first.feature/<name>— one branch per file or feature (e.g.,feature/auth-screen)fix/<name>— for bug fixes (e.g.,fix/sign-in-redirect)devis branched offmainonce at project start — allfeature/*andfix/*branches stem fromdev- Never merge branches — all merges are handled manually by the developer in GitHub
- At the start of every new session, before creating any branch or writing any code:
- Ensure you are on
dev:git checkout dev - Pull the latest changes from origin:
git pull origin dev - Only then create a new feature branch off the updated
dev
- Ensure you are on
- NEVER commit directly to
mainordev - Always branch off
devwhen starting a new file or feature - One branch per task — do not bundle unrelated changes
- After completing a task, do the following in order:
- Stage only the files relevant to the task:
git add <file> - Commit with a clean, descriptive message:
git commit -m "<type>(<scope>): <short description>" - Push the feature branch to GitHub:
git push origin <branch-name> - Merge the feature branch into
dev:git checkout dev && git merge <branch-name> - Push the updated
devbranch:git push origin dev - Delete the feature branch locally and remotely:
git branch -d <branch-name>git push origin --delete <branch-name> - Return to
devas the working branch for the next task
- Stage only the files relevant to the task:
- Commit messages must be clean and descriptive — do not reference AI, Claude, or automated tooling
<type>(<scope>): <short description>
Types: feat, fix, chore, refactor, test, docs
Examples:
feat(auth): add signInWithGoogle functionfix(booking): correct deposit calculationtest(validators): add unit tests for containsFlaggedContentfeat(tracking): add live GPS provider map screen
- At the start of every session, always run
git checkout dev && git pull origin devbefore beginning any task. - Before writing code for a new feature, state your approach and confirm it aligns with ARCHITECTURE.md.
- When modifying existing code, only touch what is necessary — do not refactor surrounding code.
- If something is unclear about domain logic, ask rather than assume.
- After completing a task, update ARCHITECTURE.md if new files, models, or patterns were introduced.
- Before starting any new feature, check
Blueprint/build_checklist.mdto confirm the correct build order and mark tasks complete with an "x" as you go.
ARCHITECTURE.md— ERD, table decisions, key design patterns. Read before writing any new file.Blueprint/schema_policies.sql— Unified merged schema with RLS policies. Source of truth for all table structures.Blueprint/dependencies_list— All approved packages. Check before installing anything new.Blueprint/build_checklist.md— Phase-by-phase build order. Check before starting any new feature.
After every completed git cycle, append a concise one-line note about the file that was just built to Blueprint/reference.md.
Format:
[path/to/file.ts] — <one sentence describing what this file does and why it exists in the app>
Example:
[src/design/tokens.ts] — Single source of truth for all color, spacing, radius, and typography values; imported by every component to enforce visual consistency.
Rules:
- One entry per completed task — do not batch multiple files into one note
- Append only — never edit or delete existing entries
- Keep the description to one sentence, focused on purpose and role in the app