# Protect Supabase Service Role Key in Expo

> The Supabase service_role key bypasses row level security, so it must never appear in an Expo app or OTA update. Keep the anon key in the app for normal queries and send privileged reads to a RealComposer pipeline that holds service_role in the vault and returns filtered fields.

- Canonical page: https://realcomposer.com/recipes/protect-supabase-service-role-key-expo
- Client platform: Expo
- Upstream services: Supabase
- Category: Security & Secret Vault
- JSON version: https://realcomposer.com/recipes/protect-supabase-service-role-key-expo.json
- Install in RealComposer (free plan, pre-fills this pipeline): https://realcomposer.com/dashboard/pipelines/new?recipe=protect-supabase-service-role-key-expo&via=agent

## Problem: Why service_role keys must stay off Expo devices

Some features, such as leaderboards or admin counts, need queries that row level security would block. Putting service_role in the app solves it quickly but hands every user full read and write access to the database.

- Full database access from a key extracted from the bundle
- Row level security bypassed for every client
- Rotation requiring a forced app update

## Solution steps

1. **Store credentials in Secret Vault**: Save SUPABASE_SERVICE_ROLE_KEY in RealComposer so the value never enters the Expo project.

2. **Configure the edge pipeline**: Add the upstream steps with vault header injection, set allowed origins, and map only the fields the screen binds.

3. **Fetch privileged data from Expo**: Call your workspace edge URL with a restricted rc_live_ client token.

```
const res = await fetch('https://realcomposer.com/v1/workspace/protect-supabase-service-role-key-expo', {
  headers: { Authorization: 'Bearer rc_live_client_token' },
});
const { leaders } = await res.json();
```

## Pipeline config

Import this config into a RealComposer pipeline. `{{secrets.NAME}}` values are read from the encrypted Secret Vault at request time, so the client never sees them.

```json
{
  "name": "Protect Supabase Service Role Key in Expo",
  "method": "GET",
  "cacheTtlSeconds": 60,
  "authMode": "client_token",
  "steps": [
    {
      "stepKey": "supabase",
      "upstreamUrlTemplate": "https://{{secrets.SUPABASE_PROJECT_REF}}.supabase.co/rest/v1/scores?select=display_name,points,user_id&order=points.desc&limit=3",
      "httpMethod": "GET",
      "headersTemplate": {
        "apikey": "{{secrets.SUPABASE_SERVICE_ROLE_KEY}}",
        "Authorization": "Bearer {{secrets.SUPABASE_SERVICE_ROLE_KEY}}"
      },
      "timeoutMs": 3000
    }
  ],
  "outputMapping": {
    "version": 1,
    "fields": [
      {
        "targetPath": "leaders.0.name",
        "sourceStep": "supabase",
        "sourcePath": "0.display_name"
      },
      {
        "targetPath": "leaders.0.points",
        "sourceStep": "supabase",
        "sourcePath": "0.points",
        "type": "number"
      },
      {
        "targetPath": "leaders.1.name",
        "sourceStep": "supabase",
        "sourcePath": "1.display_name"
      },
      {
        "targetPath": "leaders.1.points",
        "sourceStep": "supabase",
        "sourcePath": "1.points",
        "type": "number"
      },
      {
        "targetPath": "leaders.2.name",
        "sourceStep": "supabase",
        "sourcePath": "2.display_name"
      },
      {
        "targetPath": "leaders.2.points",
        "sourceStep": "supabase",
        "sourcePath": "2.points",
        "type": "number"
      }
    ]
  }
}
```

## Call the pipeline

```bash
curl -X GET "https://realcomposer.com/v1/workspace/protect-supabase-service-role-key-expo" \
  -H "Authorization: Bearer rc_live_client_token"
```

## Payload before and after

Raw upstream response:

```json
[
  {
    "display_name": "ada",
    "points": 980,
    "user_id": "u_1"
  },
  {
    "display_name": "grace",
    "points": 940,
    "user_id": "u_2"
  },
  {
    "display_name": "linus",
    "points": 910,
    "user_id": "u_3"
  }
]
```

Slimmed RealComposer response:

```json
{
  "leaders": [
    {
      "name": "ada",
      "points": 980
    },
    {
      "name": "grace",
      "points": 940
    },
    {
      "name": "linus",
      "points": 910
    }
  ]
}
```

## FAQ

### Is the Supabase anon key safe in Expo?

Yes, when row level security is enabled on every table. The anon key is designed to be public; service_role is not.

### Why cache the leaderboard for 60 seconds?

Every user sees the same top three, so a short edge cache serves thousands of requests from one Supabase query.
