Life Partner Seek
Matchmaking platform built for a proposed marriage bureau — complete and deployable, with a role-based visibility model built to the same standard as an enterprise RBAC system.
A family member was planning to launch a matrimonial matchmaking bureau — the traditional model where bride and groom candidates, often with a parent managing the profile on their behalf, are matched by staff who mediate between families. That staffing model creates a real access-control problem: members need to browse and message each other, parents need visibility into a profile they manage without holding that person's login, and bureau staff (relationship managers, admins) need broad cross-profile access to do their job — but a member shouldn't see everyone's full profile, and even staff identity shouldn't be exposed to a member who has no active relationship with them.
Life Partner Seek was built to solve that access problem in the data layer, not paper over it with UI-level hiding that a direct API call could bypass.
Bride, groom, and parent members browsing and managing matrimonial profiles, plus relationship-manager and admin roles staffing the bureau. Built for a family member who's planning to launch this as a matchmaking bureau — it hasn't launched yet, so there are no real members using it, and this write-up doesn't claim otherwise.
Solo engineering team, alongside a full-time role at TCS — product design, data model, and full-stack build (React/TypeScript frontend, Supabase schema and RLS policies).
Five roles, one enum, one lookup table. app_role — bride, groom, parent, relationship_manager, admin — is a Postgres enum, and role membership lives in a dedicated user_roles table rather than as a column on profiles. That separation is deliberate: it's the standard defense against a user editing their own profile row to grant themselves a role they shouldn't have.
One function gates every decision. has_role(user_id, role) is a SECURITY DEFINER SQL function that checks user_roles, and it's the only thing every RLS policy calls to make an access decision — no role logic duplicated across policies or reimplemented in the frontend.
Staff access is real, but staff identity isn't free. Admins and relationship managers can see across member profiles to do their job. But a RESTRICTIVE policy on profiles (the snippet below) means a member can only see who a given admin or RM actually is if they have a genuine relationship with that staff member — themselves, an active managed_profiles link, or an existing message thread. Role membership alone doesn't unlock visibility in either direction by default.
Parents get delegated access, not shared logins. A managed_profiles table links a manager_id to a managed_profile_id with an explicit relationship (parent or relationship_manager) — so a parent managing their child's profile is a real, auditable row in the data model, not a shared password.
-- Allow staff names (Admin/RM) to be visible to users who have a direct message relationship with them
-- This updates the restrictive profiles visibility policy to include has_message_relationship()
DROP POLICY IF EXISTS "Restrict staff visibility" ON public.profiles;
CREATE POLICY "Restrict staff visibility"
ON public.profiles
AS RESTRICTIVE
FOR SELECT
USING (
-- Non-staff profiles are generally visible subject to other policies
(
NOT has_role(id, 'admin'::app_role)
AND NOT has_role(id, 'relationship_manager'::app_role)
)
OR
-- Staff profiles (admin/RM) are visible if one of these conditions holds
(
(has_role(id, 'admin'::app_role) OR has_role(id, 'relationship_manager'::app_role))
AND (
auth.uid() = id -- self
OR has_role(auth.uid(), 'admin'::app_role) -- admin viewing
OR has_role(auth.uid(), 'relationship_manager'::app_role) -- RM viewing
OR EXISTS (
SELECT 1
FROM public.managed_profiles mp
WHERE mp.manager_id = profiles.id
AND mp.managed_profile_id = auth.uid()
)
OR has_message_relationship(auth.uid(), id) -- has DM relationship with this staff
)
)
);
Complete and deployable, but not launched — it's waiting on its operator (the family member it was built for) to decide on go-to-market, so there are no real members or usage numbers to report yet.
It was built as though it were going into production from day one regardless: the role model wasn't bolted on after an MVP, it was there from the first schema migration, and the access-control decisions are enforced by Postgres RLS rather than trusted to the frontend.