Mawaddah
Full-stack matchmaking portal with relational preference search and Supabase services.
Project Overview#
A freelance client hired me to build Mawaddah, a full-stack matchmaking web platform and administrative review portal. The client had previously collected profile data manually via Google Forms and spreadsheets, which made candidate matching slow, unorganized, and prone to privacy leaks.
I built the application using Next.js App Router, TypeScript, and Supabase for backend authentication, database storage, and access controls over a PostgreSQL database.
Core Features & What I Built#
- Multi-Step Onboarding: An onboarding wizard where members enter demographic details, education, occupation, and specific partner criteria.
- Multi-Filter Candidate Search: Users can search and filter prospective matches across age range, city, education level, and lifestyle criteria.
- Data Privacy & Contact Masking: Sensitive contact details like phone numbers and email addresses remain masked until both parties mutually accept a connection request.
- Admin Moderation Dashboard: A private administrative portal where the client reviews new profile submissions, verifies credentials, and approves profiles before they go live.
Technical Challenges & How I Solved Them#
1. Speeding Up Multi-Filter Searches (350ms → 120ms)
In early prototypes, searching across multiple filter parameters (age range, city, and education) caused full table scans across the member table, leading to sluggish search responses (~350ms on test data).
What I did:
- Created composite B-tree indexes in PostgreSQL covering the most common filter combinations (
gender,city,education_level). - Implemented sparse field selection in database queries, fetching only the lightweight summary fields needed to render preview cards rather than loading full biographical records on the search page. This dropped response times to ~120ms.
2. Protecting Member Privacy with Row Level Security (RLS)
Because matchmaking involves personal information, contact information had to remain private during initial browsing.
What I did:
- Configured PostgreSQL Row Level Security (RLS) policies in Supabase to restrict column-level reads based on connection status.
- Contact numbers and personal identifiers are only returned when an explicit, mutually-accepted
connectionrecord exists between both authenticated users, preventing unauthorized data scraping.
Architecture#
Next.js Client (App Router)
│
▼
┌────────────────────────────────────────┐
│ Supabase Auth & Client │ (JWT Sessions, Role-Based Access)
└───────────────────┬────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ Supabase Row Level Security │ (Data Privacy & Contact Masking)
└───────────────────┬────────────────────┘
│
▼
┌────────────────────────────────────────┐
│ PostgreSQL (Indexed Tables) │ (Composite B-Tree Indexes: 350ms → 120ms)
└────────────────────────────────────────┘What I Learned#
- Database-level RLS prevents accidental data leaks: Moving privacy checks into Supabase Row Level Security ensures that sensitive columns are protected at the database tier, eliminating the risk of accidental exposure in API routes.
- Composite indexes keep multi-filter searches instant: Analyzing query plans and adding composite indexes on frequently combined filter attributes dramatically reduces query latency as records grow.