Skip to content

Commit 5085039

Browse files
committed
docs: add RLS issue documentation and utility functions
- Create codev-queries.ts utility to prevent role_id corruption - Provides fetchAllCodevsWithCorrectRoles() function - Convenience wrappers: fetchMentors(), fetchAdmins(), fetchCodevs() - Exports ROLE_IDS constants for consistency - Add comprehensive RLS documentation in docs/RLS-KNOWN-ISSUES.md - Documents the role_id corruption issue - Provides code examples of wrong vs correct patterns - Lists affected files and solutions - Testing guidelines for future developers This prevents the RLS role_id corruption issue from being repeated in future code by providing reusable utilities and clear documentation.
1 parent c65c439 commit 5085039

2 files changed

Lines changed: 287 additions & 0 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Codev Query Utilities
3+
*
4+
* IMPORTANT: These utilities prevent RLS policy issues with the role_id field.
5+
*
6+
* ### Known Issue: RLS Role ID Corruption
7+
* When querying all users without filtering by role_id, Supabase RLS policies
8+
* corrupt the role_id field, returning incorrect values:
9+
*
10+
* ❌ WRONG:
11+
* ```ts
12+
* const { data } = await supabase.from("codev").select("*");
13+
* // Returns 1000 users, but only 5 have correct role_id values
14+
* ```
15+
*
16+
* ✅ CORRECT:
17+
* ```ts
18+
* const { data } = await supabase.from("codev").select("*").eq("role_id", 5);
19+
* // Returns all 11 mentors with correct role_id values
20+
* ```
21+
*
22+
* ### Solution
23+
* Always use `fetchCodevsByRole()` or `fetchAllCodevsWithCorrectRoles()`
24+
* instead of direct queries when you need accurate role_id data.
25+
*/
26+
27+
import { createClient } from "@supabase/supabase-js";
28+
29+
export const ROLE_IDS = {
30+
ADMIN: 1,
31+
MENTOR: 5,
32+
APPLICANT: 7,
33+
CODEV: 10,
34+
} as const;
35+
36+
interface CodevQueryOptions {
37+
selectFields?: string;
38+
includeNullRoles?: boolean;
39+
}
40+
41+
/**
42+
* Fetch codevs by a specific role ID
43+
* This ensures role_id field is returned correctly by RLS policies
44+
*
45+
* @param roleId - The role ID to filter by (use ROLE_IDS constants)
46+
* @param options - Query options
47+
*/
48+
export async function fetchCodevsByRole(
49+
roleId: number,
50+
options: CodevQueryOptions = {}
51+
) {
52+
const supabase = createClient(
53+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
54+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
55+
);
56+
57+
const selectFields = options.selectFields || "*";
58+
59+
const { data, error } = await supabase
60+
.from("codev")
61+
.select(selectFields)
62+
.eq("role_id", roleId);
63+
64+
if (error) {
65+
console.error(`Error fetching codevs with role_id ${roleId}:`, error);
66+
return [];
67+
}
68+
69+
return data || [];
70+
}
71+
72+
/**
73+
* Fetch ALL codevs with correct role_id values
74+
*
75+
* Queries each role separately to avoid RLS corruption, then combines results.
76+
* Use this instead of a single query when you need accurate role_id data.
77+
*
78+
* @param options - Query options
79+
*/
80+
export async function fetchAllCodevsWithCorrectRoles(
81+
options: CodevQueryOptions = {}
82+
) {
83+
const supabase = createClient(
84+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
85+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
86+
);
87+
88+
const selectFields = options.selectFields || "*";
89+
const roleIds = [
90+
ROLE_IDS.ADMIN,
91+
ROLE_IDS.MENTOR,
92+
ROLE_IDS.APPLICANT,
93+
ROLE_IDS.CODEV,
94+
];
95+
96+
// Build queries for each role
97+
const queries = roleIds.map((roleId) =>
98+
supabase.from("codev").select(selectFields).eq("role_id", roleId)
99+
);
100+
101+
// Optionally include users with null role_id
102+
if (options.includeNullRoles) {
103+
queries.push(
104+
supabase.from("codev").select(selectFields).is("role_id", null)
105+
);
106+
}
107+
108+
// Execute all queries in parallel
109+
const results = await Promise.all(queries);
110+
111+
// Combine results
112+
let allCodevs: any[] = [];
113+
results.forEach(({ data, error }, index) => {
114+
if (error) {
115+
const roleId = index < roleIds.length ? roleIds[index] : "null";
116+
console.error(`Error fetching role_id ${roleId}:`, error);
117+
} else if (data) {
118+
allCodevs = allCodevs.concat(data);
119+
}
120+
});
121+
122+
return allCodevs;
123+
}
124+
125+
/**
126+
* Fetch mentors (role_id = 5)
127+
* Convenience wrapper around fetchCodevsByRole
128+
*/
129+
export async function fetchMentors(selectFields?: string) {
130+
return fetchCodevsByRole(ROLE_IDS.MENTOR, { selectFields });
131+
}
132+
133+
/**
134+
* Fetch admins (role_id = 1)
135+
* Convenience wrapper around fetchCodevsByRole
136+
*/
137+
export async function fetchAdmins(selectFields?: string) {
138+
return fetchCodevsByRole(ROLE_IDS.ADMIN, { selectFields });
139+
}
140+
141+
/**
142+
* Fetch codevs (role_id = 10)
143+
* Convenience wrapper around fetchCodevsByRole
144+
*/
145+
export async function fetchCodevs(selectFields?: string) {
146+
return fetchCodevsByRole(ROLE_IDS.CODEV, { selectFields });
147+
}

‎docs/RLS-KNOWN-ISSUES.md‎

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Supabase RLS Known Issues
2+
3+
## 🔴 Critical: role_id Field Corruption
4+
5+
### The Problem
6+
7+
When querying the `codev` table **without** explicitly filtering by `role_id`, Supabase Row Level Security (RLS) policies corrupt the `role_id` field, returning incorrect values for most users.
8+
9+
### Example
10+
11+
```typescript
12+
// ❌ WRONG - Returns incorrect role_id values
13+
const { data } = await supabase
14+
.from("codev")
15+
.select("*");
16+
17+
// Result: 1000 users returned, but only 5 have correct role_id values
18+
// The other 995 users have corrupted/wrong role_id values
19+
20+
// ✅ CORRECT - Returns accurate role_id values
21+
const { data } = await supabase
22+
.from("codev")
23+
.select("*")
24+
.eq("role_id", 5);
25+
26+
// Result: All 11 mentors with correct role_id = 5
27+
```
28+
29+
### Root Cause
30+
31+
The RLS policies on the `codev` table appear to have a join-based filtering mechanism that silently corrupts the `role_id` field when:
32+
1. Fetching all users without role filtering
33+
2. Using certain joins with other tables (like `project_members`)
34+
35+
This was discovered when mentor counts didn't match:
36+
- Database: 11 users with `role_id = 5` (Mentors)
37+
- Query without filter: Only 5 users had `role_id = 5`
38+
- Query with `.eq("role_id", 5)`: All 11 users correctly returned
39+
40+
### Solutions
41+
42+
#### 1. Use the Utility Functions (Recommended)
43+
44+
Import and use the utility functions from `/lib/server/codev-queries.ts`:
45+
46+
```typescript
47+
import {
48+
fetchAllCodevsWithCorrectRoles,
49+
fetchMentors,
50+
fetchAdmins,
51+
ROLE_IDS
52+
} from "@/lib/server/codev-queries";
53+
54+
// Fetch all users with correct role_id values
55+
const allUsers = await fetchAllCodevsWithCorrectRoles();
56+
57+
// Fetch only mentors
58+
const mentors = await fetchMentors();
59+
60+
// Fetch only admins
61+
const admins = await fetchAdmins();
62+
63+
// Fetch a specific role
64+
import { fetchCodevsByRole } from "@/lib/server/codev-queries";
65+
const applicants = await fetchCodevsByRole(ROLE_IDS.APPLICANT);
66+
```
67+
68+
#### 2. Manual Query Pattern
69+
70+
If you need to query manually, use separate queries per role:
71+
72+
```typescript
73+
const roleIds = [1, 5, 7, 10]; // Admin, Mentor, Applicant, Codev
74+
75+
const queries = roleIds.map(roleId =>
76+
supabase.from("codev").select("*").eq("role_id", roleId)
77+
);
78+
79+
const results = await Promise.all(queries);
80+
81+
// Combine results
82+
let allUsers = [];
83+
results.forEach(({ data }) => {
84+
if (data) allUsers = allUsers.concat(data);
85+
});
86+
```
87+
88+
#### 3. Use Anon Client
89+
90+
When possible, use the anonymous client instead of authenticated client:
91+
92+
```typescript
93+
import { createClient } from "@supabase/supabase-js";
94+
95+
const supabase = createClient(
96+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
97+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
98+
);
99+
```
100+
101+
The anon client bypasses user-specific RLS policies that might cause corruption.
102+
103+
### Files Using the Correct Pattern
104+
105+
- ✅ `apps/codebility/app/(marketing)/_components/landing/LandingAdmins.tsx`
106+
- ✅ `apps/codebility/app/home/projects/actions.ts` (`getProjectCodevs`)
107+
- ✅ `apps/codebility/components/ui/SelectMemberModal.tsx`
108+
- ✅ `apps/codebility/app/home/my-team/AddMembersModal.tsx`
109+
110+
### Testing for This Issue
111+
112+
If you suspect role_id corruption, add debug logging:
113+
114+
```typescript
115+
const { data } = await supabase.from("codev").select("*");
116+
117+
console.log('Total users:', data.length);
118+
console.log('Mentors (role_id=5):', data.filter(u => u.role_id === 5).length);
119+
console.log('Admins (role_id=1):', data.filter(u => u.role_id === 1).length);
120+
121+
// Compare with direct database query to see if counts match
122+
```
123+
124+
### Related Issues
125+
126+
- **Issue**: Only 5 of 11 mentors showing in project edit modal
127+
- **Fixed**: April 27, 2026 (commit c65c4396)
128+
- **Pattern**: Use separate role-based queries instead of fetching all users at once
129+
130+
### Future Prevention
131+
132+
1. **Code Review**: Check any new `codev` queries for role_id usage
133+
2. **Utility First**: Always use utility functions from `codev-queries.ts`
134+
3. **Testing**: Verify user counts match database when implementing features
135+
4. **Documentation**: Update this file when new RLS issues are discovered
136+
137+
---
138+
139+
**Last Updated**: April 27, 2026
140+
**Discovered By**: Zeff (investigating mentor count discrepancy)

0 commit comments

Comments
 (0)