> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstflow.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Row Level Security

> How FirstFlow's Supabase tables are protected: the anon key writes under RLS from the browser, the service-role key bypasses it on the server, and how to tighten policies around your user ids.

The browser runtime writes to Supabase with the public `anon` key, so the only thing standing between an end user and the `firstflow_*` tables is **Row Level Security** (RLS). This page explains the model the migrations ship with, the two access paths, and how to tighten policies for production.

<Warning>
  The `anon` key is in your client bundle and visible to anyone. RLS not the
  key is what keeps one user from reading or writing another user's rows.
  Review your policies before going to production.
</Warning>

## Two access paths

| Path    | Key            | RLS          | Used by                                                        |
| ------- | -------------- | ------------ | -------------------------------------------------------------- |
| Browser | `anon`         | **Enforced** | `@firstflow/runtime` persistence (flow runs)                   |
| Server  | `service_role` | **Bypassed** | `@firstflow/runtime-server` (LLM calls, conversations, traces) |

The `service_role` key bypasses RLS by design it is trusted backend code. Keep it server-only (never `NEXT_PUBLIC_*`, never committed). All untrusted, browser-originated writes go through the `anon` key and are subject to your policies.

## What the migrations ship

`0001_initial.sql` creates the tables and `0002_strict_rls.sql` **enables RLS** on them. The shipped policies are a baseline; because FirstFlow trusts whatever `user.id` your app provides (it has no auth of its own), the SDK cannot know how *your* users map to rows. Treat the shipped policies as a starting point and adapt them to your auth model.

<Note>
  Read `0002_strict_rls.sql` in the repo before relying on the defaults, and
  confirm the policies match how your app authenticates to Supabase (a Supabase
  Auth session, a signed JWT, or anon-only).
</Note>

## Tightening policies

The right policy depends on how your browser client authenticates to Supabase:

* **You use Supabase Auth in your app.** The signed-in user's JWT carries `auth.uid()`. Scope rows to it so users only touch their own:

  ```sql theme={null}
  -- Illustrative pattern adapt column/claim names to your schema
  create policy "own flow runs"
    on firstflow_flow_runs
    for all
    using (user_id = auth.uid()::text)
    with check (user_id = auth.uid()::text);
  ```

* **You do not use Supabase Auth** (the browser holds only the `anon` key). The database cannot verify the `user_id` a client sends, so anon writes are effectively trust-on-write. For stronger guarantees, proxy persistence through your own backend with the `service_role` key, or mint a short-lived signed token (see `signUserToken` in the [Server SDK](/server/overview)) and verify it in a policy.

## Verifying RLS

After applying the migrations, confirm RLS is on:

```sql theme={null}
select tablename, rowsecurity
from pg_tables
where schemaname = 'public' and tablename like 'firstflow_%';
-- rowsecurity should be true for every row
```

Then test from the browser: a logged-in user should be able to read and write
their own flow runs and nothing else.

## Next

Review [what you own](/self-hosting/responsibilities) operationally, then run the
[production checklist](/self-hosting/production) before going live.
