{
  "slug": "protect-supabase-service-role-key-expo",
  "title": "Protect Supabase Service Role Key in Expo",
  "metaDescription": "Never ship a Supabase service_role key in an Expo app. RealComposer runs privileged queries at the edge and returns only the fields each screen needs.",
  "headline": "Privileged Supabase reads for Expo without service_role on device",
  "category": "security",
  "categoryLabel": "Security & Secret Vault",
  "urgency": 10,
  "clientPlatform": "Expo",
  "upstreamServices": [
    "Supabase"
  ],
  "searchIntentQuery": "supabase service role key in expo app",
  "directAnswerSummary": "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.",
  "problem": {
    "title": "Why service_role keys must stay off Expo devices",
    "explanation": "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.",
    "risks": [
      "Full database access from a key extracted from the bundle",
      "Row level security bypassed for every client",
      "Rotation requiring a forced app update"
    ]
  },
  "solutionSteps": [
    {
      "stepNumber": 1,
      "title": "Store credentials in Secret Vault",
      "description": "Save SUPABASE_SERVICE_ROLE_KEY in RealComposer so the value never enters the Expo project."
    },
    {
      "stepNumber": 2,
      "title": "Configure the edge pipeline",
      "description": "Add the upstream steps with vault header injection, set allowed origins, and map only the fields the screen binds."
    },
    {
      "stepNumber": 3,
      "title": "Fetch privileged data from Expo",
      "description": "Call your workspace edge URL with a restricted rc_live_ client token.",
      "codeSnippet": "const res = await fetch('https://realcomposer.com/v1/workspace/protect-supabase-service-role-key-expo', {\n  headers: { Authorization: 'Bearer rc_live_client_token' },\n});\nconst { leaders } = await res.json();"
    }
  ],
  "pipelineConfig": {
    "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"
        }
      ]
    }
  },
  "sampleBloatedInput": [
    {
      "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"
    }
  ],
  "sampleSlimmedOutput": {
    "leaders": [
      {
        "name": "ada",
        "points": 980
      },
      {
        "name": "grace",
        "points": 940
      },
      {
        "name": "linus",
        "points": 910
      }
    ]
  },
  "curlSnippet": "curl -X GET \"https://realcomposer.com/v1/workspace/protect-supabase-service-role-key-expo\" \\\n  -H \"Authorization: Bearer rc_live_client_token\"",
  "faqs": [
    {
      "question": "Is the Supabase anon key safe in Expo?",
      "answer": "Yes, when row level security is enabled on every table. The anon key is designed to be public; service_role is not."
    },
    {
      "question": "Why cache the leaderboard for 60 seconds?",
      "answer": "Every user sees the same top three, so a short edge cache serves thousands of requests from one Supabase query."
    }
  ],
  "relatedRecipeSlugs": [
    "protect-supabase-service-role-key-nextjs",
    "hide-anthropic-api-key-expo",
    "stripe-proxy-react-native"
  ],
  "links": {
    "page": "https://realcomposer.com/recipes/protect-supabase-service-role-key-expo",
    "markdown": "https://realcomposer.com/recipes/protect-supabase-service-role-key-expo.md",
    "json": "https://realcomposer.com/recipes/protect-supabase-service-role-key-expo.json",
    "install": "https://realcomposer.com/dashboard/pipelines/new?recipe=protect-supabase-service-role-key-expo&via=agent"
  }
}