userType: external

gitBranch: main

slug: logical-humming-sky

type: user

role: user

content:
Fix RLS policies in Supabase database using the Management API. Run these SQL fixes:

**1. Lock down notifications INSERT policy** - currently allows any authenticated user to insert notifications for any other user. Change to service_role only:

```bash
curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "DROP POLICY IF EXISTS \"System can insert notifications\" ON public.notifications; CREATE POLICY \"Service role can insert notifications\" ON public.notifications FOR INSERT TO service_role WITH CHECK (true);"}'
```

Wait - actually that would break the app since notifications are inserted by authenticated users in the send message route. Let me check the code... The send message API at /api/messages/[id]/send/route.ts inserts notifications using the user's supabase client (not admin). So we need to restrict it to: authenticated users can only insert notifications where profile_id is NOT their own id (they can notify others but only through proper API routes), OR we can use a more permissive policy.

Actually the better fix is: Allow authenticated users to insert notifications, but the with_check should ensure the profile_id belongs to someone other than the inserter. This prevents self-notification spam. But actually the real concern is that any user can craft a notification for ANY other user with fake content. The proper fix is to restrict inserts to service_role only and update the code to use the admin client for notification inserts.

Since fixing all the code to use admin client is a larger change, let's instead add a reasonable policy that at minimum prevents users from sending notifications to themselves:

```sql
DROP POLICY IF EXISTS "System can insert notifications" ON public.notifications;
CREATE POLICY "Authenticated users can insert notifications" ON public.notifications
  FOR INSERT TO authenticated
  WITH CHECK (profile_id != auth.uid());
```

**2. Add DELETE policies for tables where users need to delete their own records:**

```sql
-- Users can delete their own notifications
CREATE POLICY "Users can delete their own notifications" ON public.notifications
  FOR DELETE TO authenticated
  USING (profile_id = auth.uid());

-- Users can delete their own messages
CREATE POLICY "Users can delete their own messages" ON public.messages
  FOR DELETE TO authenticated
  USING (sender_id = auth.uid());

-- Contractors can delete their own jobs (only draft/open ones to be safe)
CREATE POLICY "Contractors can delete their own jobs" ON public.jobs
  FOR DELETE TO authenticated
  USING (company_id IN (SELECT id FROM public.companies WHERE profile_id = auth.uid()));

-- Workers can withdraw (delete) their own match applications
CREATE POLICY "Workers can withdraw their own applications" ON public.matches
  FOR DELETE TO authenticated
  USING (worker_id = auth.uid() AND status IN ('applied', 'suggested'));
```

Run each of these as separate curl commands against the Management API. Verify they were created successfully.

The Management API endpoint is:
```
curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "YOUR SQL HERE"}'
```

After creating all policies, run a verification query to list all policies and confirm the new ones exist.


type: message

role: assistant

type: text

text: I'll run all the SQL fixes as separate curl commands against the Management API. Let me execute them in parallel since they're independent operations.

service_tier: standard

type: assistant


Bash

command:
curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "DROP POLICY IF EXISTS \"System can insert notifications\" ON public.notifications; CREATE POLICY \"Authenticated users can insert notifications\" ON public.notifications FOR INSERT TO authenticated WITH CHECK (profile_id != auth.uid());"}'

description: Fix notifications INSERT policy to prevent self-notification

type: direct


message: Bad escaped character in JSON at position 233 (line 1 column 234)


command:
curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "CREATE POLICY \"Users can delete their own notifications\" ON public.notifications FOR DELETE TO authenticated USING (profile_id = auth.uid());"}'

description: Add DELETE policy for notifications


message: Format is Authorization: Bearer [token]


command:
curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "CREATE POLICY \"Users can delete their own messages\" ON public.messages FOR DELETE TO authenticated USING (sender_id = auth.uid());"}'

description: Add DELETE policy for messages


command:
curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "CREATE POLICY \"Contractors can delete their own jobs\" ON public.jobs FOR DELETE TO authenticated USING (company_id IN (SELECT id FROM public.companies WHERE profile_id = auth.uid()));"}'

description: Add DELETE policy for jobs


command:
curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "CREATE POLICY \"Workers can withdraw their own applications\" ON public.matches FOR DELETE TO authenticated USING (worker_id = auth.uid() AND status IN ('\''applied'\'', '\''suggested'\''));"}'

description: Add DELETE policy for matches (withdraw applications)


text: The token issue and JSON escaping need to be addressed. Let me check the access token and fix the approach.


command: echo "Token length: ${#SUPABASE_ACCESS_TOKEN}" && echo "Token starts with: ${SUPABASE_ACCESS_TOKEN:0:10}"

description: Check if SUPABASE_ACCESS_TOKEN is set


content:
Token length: 0
Token starts with:


text: The token is not set in the current shell. Let me source the bashrc to pick it up.


command: source ~/.bashrc && echo "Token length: ${#SUPABASE_ACCESS_TOKEN}" && echo "Token starts with: ${SUPABASE_ACCESS_TOKEN:0:10}"

description: Source bashrc and check token


command: source ~/.zshrc 2>/dev/null; source ~/.profile 2>/dev/null; source ~/.bash_profile 2>/dev/null; echo "Token length: ${#SUPABASE_ACCESS_TOKEN}"

description: Try other profile files for the token


content: Token length: 0


command: grep -r "SUPABASE_ACCESS_TOKEN" ~/.bashrc ~/.zshrc ~/.profile ~/.bash_profile ~/the-50-dollar-app/.env* 2>/dev/null | head -20

description: Search for where the token is defined


content: /home/ccuser/.bashrc:export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5"


text: Found it. Let me source the bashrc properly and run all the fixes.


command: export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5" && echo "Token set, length: ${#SUPABASE_ACCESS_TOKEN}"

description: Manually set the access token


content: Token set, length: 44


text: Good, the token is set. Now let me run all the policy fixes. I'll use heredocs to avoid JSON escaping issues.


command:
export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5"

curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(cat <<'ENDJSON'
{"query": "DROP POLICY IF EXISTS \"System can insert notifications\" ON public.notifications; CREATE POLICY \"Authenticated users can insert notifications\" ON public.notifications FOR INSERT TO authenticated WITH CHECK (profile_id != auth.uid());"}
ENDJSON
)"

description: Fix notifications INSERT policy - prevent self-notification


text: Empty array means success. Now let me run the remaining four DELETE policies in parallel.


command:
export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5"

curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(cat <<'ENDJSON'
{"query": "CREATE POLICY \"Users can delete their own notifications\" ON public.notifications FOR DELETE TO authenticated USING (profile_id = auth.uid());"}
ENDJSON
)"

description: Add DELETE policy for notifications


command:
export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5"

curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(cat <<'ENDJSON'
{"query": "CREATE POLICY \"Users can delete their own messages\" ON public.messages FOR DELETE TO authenticated USING (sender_id = auth.uid());"}
ENDJSON
)"

description: Add DELETE policy for messages


command:
export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5"

curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(cat <<'ENDJSON'
{"query": "CREATE POLICY \"Contractors can delete their own jobs\" ON public.jobs FOR DELETE TO authenticated USING (company_id IN (SELECT id FROM public.companies WHERE profile_id = auth.uid()));"}
ENDJSON
)"

description: Add DELETE policy for jobs


command:
export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5"

curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(cat <<'ENDJSON'
{"query": "CREATE POLICY \"Workers can withdraw their own applications\" ON public.matches FOR DELETE TO authenticated USING (worker_id = auth.uid() AND status IN ('applied', 'suggested'));"}
ENDJSON
)"

description: Add DELETE policy for matches (withdraw applications)


text: All five policies created successfully (empty array `[]` = success, no errors). Now let me run the verification query to list all policies and confirm everything is in place.


command:
export SUPABASE_ACCESS_TOKEN="sbp_06aeb0c9b033c7bdffd8967c9b4769156137dac5"

curl -s "https://api.supabase.com/v1/projects/eciepjpcyfurbkfzekok/database/query" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(cat <<'ENDJSON'
{"query": "SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check FROM pg_policies WHERE schemaname = 'public' ORDER BY tablename, cmd, policyname;"}
ENDJSON
)" | python3 -m json.tool

description: Verify all RLS policies across public schema tables


schemaname: public

tablename: companies

policyname: Contractors can insert their own company

permissive: PERMISSIVE

roles: {public}

cmd: INSERT

with_check: (profile_id = auth.uid())

policyname: Companies are viewable by everyone

cmd: SELECT

qual: true

policyname: Contractors can update their own company

cmd: UPDATE

qual: (profile_id = auth.uid())

policyname: Users can insert conversation participants

policyname: Users can view participants of their conversations

qual:
(conversation_id IN ( SELECT conversation_participants_1.conversation_id
   FROM conversation_participants conversation_participants_1
  WHERE (conversation_participants_1.profile_id = auth.uid())))

policyname: Users can update their own participant record

tablename: conversations

policyname: Users can view their own conversations

qual:
(id IN ( SELECT conversation_participants.conversation_id
   FROM conversation_participants
  WHERE (conversation_participants.profile_id = auth.uid())))

tablename: jobs

policyname: Contractors can delete their own jobs

roles: {authenticated}

cmd: DELETE

qual:
(company_id IN ( SELECT companies.id
   FROM companies
  WHERE (companies.profile_id = auth.uid())))

policyname: Contractors can insert jobs for their company

policyname: Jobs are viewable by everyone

policyname: Contractors can update their own jobs

tablename: matches

policyname: Workers can withdraw their own applications

qual: ((worker_id = auth.uid()) AND (status = ANY (ARRAY['applied'::match_status, 'suggested'::match_status])))

policyname: Secure matches insert

with_check:
(((worker_id = auth.uid()) AND (status = 'applied'::match_status)) OR ((job_id IN ( SELECT j.id
   FROM (jobs j
     JOIN companies c ON ((j.company_id = c.id)))
  WHERE (c.profile_id = auth.uid()))) AND (status = 'suggested'::match_status)) OR ((auth.jwt() ->> 'role'::text) = 'service_role'::text))

policyname: Users can view their own matches

qual:
((worker_id = auth.uid()) OR (job_id IN ( SELECT j.id
   FROM (jobs j
     JOIN companies c ON ((j.company_id = c.id)))
  WHERE (c.profile_id = auth.uid()))))

policyname: Involved parties can update matches

tablename: messages

policyname: Users can delete their own messages

qual: (sender_id = auth.uid())

policyname: Users can send messages in their conversations

with_check:
((sender_id = auth.uid()) AND (conversation_id IN ( SELECT conversation_participants.conversation_id
   FROM conversation_participants
  WHERE (conversation_participants.profile_id = auth.uid()))))

policyname: Users can view messages in their conversations

policyname: Users can delete their own notifications

policyname: Authenticated users can insert notifications

policyname: Users can update their own notifications

with_check:
(((auth.jwt() ->> 'role'::text) = 'service_role'::text) AND (EXISTS ( SELECT 1
   FROM matches
  WHERE ((matches.id = payments.match_id) AND (matches.status = 'hired'::match_status)))) AND (EXISTS ( SELECT 1
   FROM (matches m
     JOIN jobs j ON ((m.job_id = j.id)))
  WHERE ((m.id = payments.match_id) AND (j.company_id = j.company_id)))))

policyname: Public profiles are viewable by everyone

policyname: Worker profiles are viewable by everyone


text:
All policies have been created and verified successfully. Here is a summary of what was done:

---

**RLS Policy Changes Applied**

**1. Notifications INSERT policy (replaced):**
- Dropped: `"System can insert notifications"` (allowed any authenticated user to insert for anyone)
- Created: `"Authenticated users can insert notifications"` -- roles: `{authenticated}`, with_check: `profile_id <> auth.uid()` -- prevents self-notification spam while still allowing the app to create notifications for other users.

**2. Four new DELETE policies added:**

| Table | Policy Name | Roles | Condition |
|-------|-------------|-------|-----------|
| `notifications` | "Users can delete their own notifications" | `{authenticated}` | `profile_id = auth.uid()` |
| `messages` | "Users can delete their own messages" | `{authenticated}` | `sender_id = auth.uid()` |
| `jobs` | "Contractors can delete their own jobs" | `{authenticated}` | `company_id IN (SELECT id FROM companies WHERE profile_id = auth.uid())` |
| `matches` | "Workers can withdraw their own applications" | `{authenticated}` | `worker_id = auth.uid() AND status IN ('applied', 'suggested')` |

All five policies are confirmed present in the verification query output. The matches DELETE policy correctly restricts withdrawal to only `applied` or `suggested` statuses, preventing deletion of matches that have progressed to `hired` or other terminal states.