Supabase Social Graph Engineer
Mission
Design the canonical social graph for follows, approvals, blocks, and derived friendship.
Required stance
Use an asymmetric follow model as the primitive. Do not use a symmetric friendship table as the foundation.
Core tables
lensers.profiles
Must include or reference:
profile_idaccount_idusernameslugvisibilityenum:public|privateaccount_statusenum:active|deactivated|pending_deletion|deleteddeleted_atdeletion_scheduled_fordeactivated_at- summary fields for restricted shell
lensers.relationships
One row per directed relationship from viewer to subject.
Columns:
source_profile_idtarget_profile_idstatusenum:pending|accepted|rejected|blocked|removedrequested_atresponded_ataccepted_atremoved_atis_close_circleboolean default falsecreated_by_policytext nullable- unique
(source_profile_id, target_profile_id)
lensers.profile_counters
Denormalized counters:
followers_countfollowing_countmutuals_countoptionalthreads_count_publicprompts_count_publicbadges_count
Optional lensers.blocks
You may keep blocks in relationships.status='blocked', but a dedicated block table is cleaner if moderation rules grow.
Required helper functions
fn_relationship_state(viewer_profile_id, subject_profile_id)
Returns:
- direct relationship status
- reverse relationship status
is_mutual_followis_blocked_any_direction
fn_can_view_profile(viewer_auth_uid, subject_profile_id)
Returns:
- access outcome
- access reason
- can_view_full_profile
- can_view_restricted_shell
- can_request_follow
- can_cancel_request
- can_unfollow
fn_request_follow(subject_profile_id)
Rules:
- public account => create accepted follow immediately
- private active account => create pending request
- deactivated / pending_deletion / deleted => reject
- blocked relation any direction => reject
fn_accept_follow_request(source_profile_id)
Only target owner may accept.
fn_remove_follow(target_profile_id)
Soft-remove relation or switch to removed.
Derived friendship
Friendship is computed, not stored as the main truth:
is_friend = exists accepted(A->B) and accepted(B->A)
Only materialize it if analytics/search need acceleration.
Counter maintenance
Use triggers or queued jobs to maintain:
- follower / following counts
- public-content counts
Favor correctness over micro-optimization.
Query rules
- Never join raw relations in ad hoc frontend queries for access decisions.
- Expose a single profile-access RPC or security-definer view.
- All search/discovery endpoints must filter
account_status='active'.
Suggested indexes
- unique
(source_profile_id, target_profile_id) - btree
(target_profile_id, status) - btree
(source_profile_id, status) - partial index for
status='pending' - partial index for
status='accepted' - index on
profiles(username) - index on
profiles(slug) - index on
profiles(account_status, visibility)
Deliverables
Produce:
- normalized schema
- constraints
- indexes
- trigger plan
- RPC contract