RoomieTab · Architecture
Architecture Document
roomietab schemaOverview
RoomieTab is a real-time roommate expense management web app for groups of up to 5 people. Built on Next.js 15 (App Router) with Supabase as the backend, it enables frictionless expense logging with flexible split rules (equal, exact, percentage, shares), tracks who paid for what, and at month-end computes the minimum number of transactions to settle all debts using a greedy net-balance algorithm.
Architecture highlights:
- Database: 7 tables in a dedicated 'roomietab' PostgreSQL schema. All monetary values stored as integer cents to eliminate floating-point errors. Row-Level Security on every table ensures strict household-scoped data isolation.
- Auth: Supabase Auth with email magic links (zero-friction) and Google OAuth. Guest members can join via shareable invite link without mandatory account creation.
- Realtime: Supabase Realtime subscriptions on expenses and settlement_transactions tables provide live sync across all connected roommates — no manual refresh needed.
- API Design: Most reads use direct Supabase client queries (protected by RLS). API routes handle complex server-side logic: settlement calculation (min-transaction algorithm), month archival, recurring expense generation, and file exports.
- Storage: Supabase Storage bucket for receipt images with 5MB limit and image-only MIME type restrictions.
- Performance: Next.js Server Components for fast initial loads, optimistic UI updates for instant feedback, and efficient composite indexes on high-query paths (household+date, household+month).
Tech Stack
framework
Next.js
framework Version
15.1
styling
Tailwind CSS 3.4 with custom design system (indigo primary, Caveat/Inter fonts, hand-drawn shadow utilities)
database
Supabase PostgreSQL with dedicated 'roomietab' schema, Row-Level Security on all tables, integer-cents monetary storage
auth
Supabase Auth with email magic link and Google OAuth, PKCE flow, cookie-based sessions via @supabase/ssr
hosting
Vercel (Edge Runtime for API routes, automatic preview deployments on PR)
ci
GitHub Actions (lint, type-check, build on push and PR to main)
Data Entities
Household
roomietab.households| Field | Type | Nullable | Description |
|---|---|---|---|
| id | uuid | no | Primary key |
| name | text | no | Household display name e.g. Maple House |
| invite_code | text | no | Unique shareable invite code for joining via link |
| created_by | uuid | no | FK to auth.users — the household creator/admin |
| created_at | timestamptz | no | Creation timestamp |
| updated_at | timestamptz | no | Last update timestamp |
Member
roomietab.members| Field | Type | Nullable | Description |
|---|---|---|---|
| id | uuid | no | Primary key |
| household_id | uuid | no | FK to households |
| user_id | uuid | yes | FK to auth.users — null for guest members who haven't signed up |
| display_name | text | no | Name shown in the app UI |
| text | yes | Email used for invitations | |
| avatar_url | text | yes | Profile photo URL from Supabase Storage |
| role | text | no | Either 'admin' or 'member' |
| venmo_handle | text | yes | Venmo username for pre-filled payment deep links |
| paypal_email | text | yes | PayPal email for pre-filled payment deep links |
| notification_prefs | jsonb | no | Push notification toggle preferences |
| is_active | boolean | no | Soft-delete flag for removed members |
| joined_at | timestamptz | no | When member joined the household |
| updated_at | timestamptz | no | Last update timestamp |
Expense
roomietab.expenses| Field | Type | Nullable | Description |
|---|---|---|---|
| id | uuid | no | Primary key |
| household_id | uuid | no | FK to households |
| description | text | no | Short expense label e.g. 'Whole Foods run' |
| amount_cents | integer | no | Total amount in cents (integer) to eliminate floating-point rounding errors |
| category | text | no | One of: rent, utilities, groceries, dining, subscriptions, transport, household, other |
| split_type | text | no | Split method: equal, exact, percentage, shares |
| paid_by_member_id | uuid | no | FK to the member who paid this expense |
| expense_date | date | no | Date the expense occurred |
| receipt_url | text | yes | URL to receipt image stored in Supabase Storage |
| is_recurring | boolean | no | Flag indicating if this was auto-generated from a recurring template |
| recurring_day | integer | yes | Day of month for display purposes (1-31) |
| recurring_template_id | uuid | yes | FK to the recurring_templates row that generated this |
| is_deleted | boolean | no | Soft-delete flag — deleted expenses hidden from UI but kept for audit |
| created_at | timestamptz | no | Creation timestamp |
| updated_at | timestamptz | no | Last update timestamp |
ExpenseSplit
roomietab.expense_splits| Field | Type | Nullable | Description |
|---|---|---|---|
| id | uuid | no | Primary key |
| expense_id | uuid | no | FK to the parent expense |
| member_id | uuid | no | FK to the member who owes this share |
| amount_cents | integer | no | This member's share in cents |
| percentage | numeric(5,2) | yes | Percentage value if split_type is percentage |
| shares | integer | yes | Number of shares if split_type is shares |
| created_at | timestamptz | no | Creation timestamp |
RecurringTemplate
roomietab.recurring_templates| Field | Type | Nullable | Description |
|---|---|---|---|
| id | uuid | no | Primary key |
| household_id | uuid | no | FK to households |
| description | text | no | Expense description template e.g. 'Netflix' |
| amount_cents | integer | no | Amount in cents |
| category | text | no | Expense category |
| split_type | text | no | Split method |
| paid_by_member_id | uuid | no | FK to default payer member |
| split_config | jsonb | no | JSON array of split member configs: [{memberId, amountCents?, percentage?, shares?}] |
| day_of_month | integer | no | Day of month to auto-create expense (1-31) |
| is_active | boolean | no | Whether template is active (soft-delete) |
| last_generated_at | timestamptz | yes | Timestamp of last auto-generated expense |
| created_at | timestamptz | no | Creation timestamp |
| updated_at | timestamptz | no | Last update timestamp |
Settlement
roomietab.settlements| Field | Type | Nullable | Description |
|---|---|---|---|
| id | uuid | no | Primary key |
| household_id | uuid | no | FK to households |
| month | date | no | First day of the settlement month (e.g. 2025-02-01) |
| is_archived | boolean | no | Whether the month has been fully archived/settled |
| archived_at | timestamptz | yes | When the month was archived |
| archived_by | uuid | yes | FK to member who performed the archive action |
| created_at | timestamptz | no | Creation timestamp |
| updated_at | timestamptz | no | Last update timestamp |
SettlementTransaction
roomietab.settlement_transactions| Field | Type | Nullable | Description |
|---|---|---|---|
| id | uuid | no | Primary key |
| settlement_id | uuid | no | FK to the parent settlement |
| payer_member_id | uuid | no | FK to member who needs to pay |
| receiver_member_id | uuid | no | FK to member who receives payment |
| amount_cents | integer | no | Transaction amount in cents |
| is_settled | boolean | no | Whether this transaction has been completed |
| settled_at | timestamptz | yes | When marked as settled |
| created_at | timestamptz | no | Creation timestamp |
API Routes
/api/householdsauthCreate a new household with the authenticated user as admin. Creates household row, initial member row, and generates invite code.
/api/households/[id]authUpdate household name. Only admin members can update.
/api/households/[id]/joinauthJoin a household via invite code. Creates a new member record linked to the authenticated user. Enforces max 5 members.
/api/households/[id]/inviteauthSend email invitations to roommates. Only admin can invite.
/api/expensesauthCreate expense with splits in a single transaction. Validates split amounts sum to total. Inserts expense + expense_splits atomically.
/api/expenses/[id]authUpdate an existing expense and its splits. Deletes old splits and re-creates. Validates user is household member.
/api/expenses/[id]authSoft-delete an expense by setting is_deleted=true. Does not remove data for audit trail.
/api/settlements/calculateauthCompute minimum settlement transactions for a given household and month using greedy net-balance matching algorithm. All amounts in cents to avoid floating-point errors.
/api/settlements/archiveauthArchive a month's settlement. Creates Settlement and SettlementTransaction records, marks month as archived. Only admin can archive.
/api/settlements/transactions/[id]authMark a settlement transaction as settled or unsettled. Any household member can toggle.
/api/recurring-templatesauthCreate a recurring expense template with split configuration.
/api/recurring-templates/[id]authUpdate a recurring expense template.
/api/recurring-templates/[id]authDeactivate a recurring template (soft-delete by setting is_active=false).
/api/recurring/generateauthEdge Function cron handler: auto-creates expenses from active recurring templates whose day_of_month matches today. Called daily by Supabase cron.
/api/export/csvauthGenerate CSV export of a household's monthly expenses. Returns downloadable CSV file.
/api/export/pdfauthGenerate PDF summary of a household's monthly expenses and settlement. Returns downloadable PDF.
/api/members/[id]authUpdate member profile: display name, avatar, payment handles, notification prefs.
/api/receipts/uploadauthUpload a receipt image to Supabase Storage. Returns the public URL for the uploaded file.
Authentication
Dependencies
| Package | Version | Purpose |
|---|---|---|
| next | 15.1 | React framework with App Router, Server Components, and API route handlers |
| react | 19.0 | UI library for building component-based interfaces |
| react-dom | 19.0 | React DOM rendering |
| @supabase/supabase-js | 2.47 | Supabase JavaScript client for database queries, auth, realtime, and storage |
| @supabase/ssr | 0.5 | Supabase server-side rendering helpers for Next.js (cookie-based session management) |
| react-hook-form | 7.54 | Performant form state management with minimal re-renders — ideal for the expense entry form |
| @hookform/resolvers | 3.9 | Connects Zod validation schemas to react-hook-form |
| zod | 3.24 | TypeScript-first schema validation for forms and API request bodies |
| lucide-react | 0.468 | Tree-shakeable icon library with 1500+ icons — used for nav, buttons, category icons |
| date-fns | 4.1 | Lightweight modular date utility for formatting expense dates and month navigation |
| sonner | 1.7 | Minimal toast notification library with built-in action buttons (for Undo on delete) |
| tailwindcss | 3.4 | Utility-first CSS framework for all styling per the design system |
| @tailwindcss/forms | 0.5 | Tailwind plugin for consistent form element styling |
| clsx | 2.1 | Tiny utility for conditionally joining CSS class names |
| tailwind-merge | 2.6 | Merge Tailwind classes without conflicts — used in cn() utility |
| @react-pdf/renderer | 4.1 | Server-side PDF generation for monthly expense export |
| typescript | 5.7 | TypeScript compiler for type safety across the codebase |
| @types/react | 19.0 | TypeScript type definitions for React |
| @types/node | 22.0 | TypeScript type definitions for Node.js |
| eslint | 9.0 | JavaScript/TypeScript linter |
| eslint-config-next | 15.1 | ESLint configuration preset for Next.js projects |
| prettier | 3.4 | Code formatter for consistent style |
| supabase | 2.0 | Supabase CLI for local development, migrations, and type generation |