# Add Row Level Security Policies to a Supabase Project from a Linear Issue

Yes. An agent picks up the Linear issue, writes row level security policies as a Supabase migration, tests them and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/add-rls-policies/supabase/), in a Project that has your repository.

## What the agent does

The agent clones the repository into its own cloud sandbox and reads the project first. In the Supabase doc search starter `supabase-community/nextjs-openai-doc-search` it finds a pnpm project (`pnpm-lock.yaml` and `pnpm-workspace.yaml` sit beside `package.json`), Next.js 13.2.4 with `@supabase/supabase-js` ^2.13.0, and no test script at all, so the tests the issue asks for are new ground rather than an existing suite to extend.

The database schema lives in `supabase/migrations/20230406025118_init.sql`. It creates the pgvector extension and two tables, `public.nods_page` and `public.nods_page_section`, and both already run `alter table ... enable row level security`. That is the catch the issue is really about: RLS is enabled with zero policies, so the `anon` role can read nothing at all, and the search function `match_page_sections(embedding vector(1536), match_threshold float, match_count int, min_content_length int)` is created without `security definer`, so it runs with the rights of whoever calls it.

The app explains why that matters. `pages/api/vector-search.ts`, an edge API route, builds a Supabase client with `SUPABASE_SERVICE_ROLE_KEY` and calls `rpc('match_page_sections', …)`, and `lib/generate-embeddings.ts` writes the sections with the service key at build time. The service role bypasses RLS by design, so embeddings flow in and search answers through it while `anon` sits behind an empty policy set. The migration the issue asks for closes the middle ground: `anon` keeps a path into the data only by executing the match function, direct `select` on both tables stays closed to it, and service-role writes keep working.

The agent writes that as a new migration under `supabase/migrations/` with the pgTAP tests beside it in `supabase/tests/`: `anon` can execute `match_page_sections` but cannot `select` from `nods_page` or `nods_page_section`, and cannot insert into either, and the tests say so against a real database. The local stack is Postgres 15 (`major_version = 15` in `supabase/config.toml`), and the agent runs `supabase db test` against it, then `pnpm build` so the Next.js app still compiles with the new migration in place.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add Row Level Security Policies to a Supabase Project. Enable row level security on every table, allow anon read only through the match function and service role writes, as a new migration with pgTAP tests.\" Do the work in this repository, add tests for it, run supabase db test and pnpm build, and open a pull request that links back to the issue.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request: one new migration file, a `supabase/tests/` directory with the pgTAP tests, and no changes to `pages/api/vector-search.ts` or the embedding writer, because the service-role path already works and the migration only rearranges what `anon` can reach.

The checks run the commands the issue names. `supabase db test` exercises each policy against a real Postgres, and `pnpm build` proves the app compiles. A reviewer reads the migration for the three promises it makes: `anon` executes the match function, direct table reads stay closed, and service-role writes are untouched. The pgTAP tests are the part to read closely, because they fail if any policy is widened later.

The session read keeps the whole record: the migration applied, each test file with its result, and the build output, so every claim in the pull request description can be traced to a command and its output.

## Run it on every Linear issue labelled agent

This job is one instance of the Linear label automation. An automation on the Project watches for `linear.issue.labeled` with the `agent` label and starts a run with this prompt, so each schema or security issue becomes a run without anyone copying text between tools.

Runs can also be started from a server that receives Linear's webhooks and POSTs the same body to `/o/{org}/sessions`, with the issue text in `input`. Set a webhook URL on the API key and Rush posts `run.completed` to it when the turn finishes, so the server learns the pull request is ready without polling.

## Why run it on Rush?

pgTAP tests only mean something against a real Postgres. On a laptop that means a local Supabase stack running before every `supabase db test`. Each run on Rush boots a sandbox where the CLI and the database start together, and the run keeps every command and its output, so the proof that `anon` cannot read a table is a recorded test, not an assertion in the pull request description.

And one issue never waits behind another. A labelled migration issue and a labelled policy issue start as two runs in two sandboxes, each with its own branch, its own migration and its own pull request.

## Frequently asked questions

### The tables already have RLS enabled. What is there to add?

Enabled with no policies means `anon` can do nothing; the starter relies on the service role for all reads and writes. The migration adds the only intended `anon` path, execution of `match_page_sections`, and the pgTAP tests pin that shape so it cannot widen unnoticed.

### Will search keep working for anon callers?

The edge route in `pages/api/vector-search.ts` uses the service role key, which bypasses RLS, so the deployed search is untouched. What changes is what a leaked `anon` key could reach: function execution, not table reads.

### What exactly do the pgTAP tests assert?

Three things: `anon` executes `match_page_sections` and gets rows back, `select` on `nods_page` and `nods_page_section` as `anon` returns zero rows, and `anon` inserts are rejected.

### Does the agent run against my production project?

No. It runs `supabase db test` against the local CLI database configured in `supabase/config.toml`, Postgres 15. Applying the migration to a hosted project is a separate, deliberate `supabase db push`.

### Can I scope the policies differently?

Yes, in the issue text. Which roles get which path through which function is the spec; a new run rewrites the migration and its tests to match.
