GitHub Copilot security guide
How to secure a Supabase app built with GitHub Copilot
Copilot can read Supabase's MCP server from three places: your IDE's MCP config, an MCP registry and the Copilot CLI's ~/.copilot/mcp-config.json. Audit each one for project scoping and read-only mode, and remove any service_role key. Then make Copilot pair every create table with RLS, hunt for policies that allow every row, and keep secret keys out of the bundle.
Last updated
Where does Copilot read its Supabase MCP config?
- In the IDE: an MCP config such as .vscode/mcp.json, or a server added from an MCP registry.
- In the Copilot CLI: ~/.copilot/mcp-config.json, with OAuth through /mcp.
- Both point at Supabase's hosted server, https://mcp.supabase.com/mcp.
- Find every place your team configured the Supabase server.
- Scope each entry to one project and turn on read-only mode, as Supabase's MCP guide describes.
- Use a development project for daily work. Connect production only when a task needs it.
What access should Copilot's MCP connection have?
Supabase names prompt injection as the main LLM-specific attack on MCP. A General Analysis research demo showed why it matters: an agent in another editor, connected as service_role, followed hidden instructions in a support ticket and copied a private table. No customer breach was reported.
- No service_role or sb_secret_ key in any MCP config file.
- Read-only by default. Writes only for the task that needs them.
- No broad access while Copilot reads text your users submitted.
How do I keep secret keys out of a Copilot-written frontend?
Client code holds the publishable key (sb_publishable_) only. Secret keys and legacy service_role keys stay on servers. Supabase is deprecating the legacy JWT keys by the end of 2026, so this is a good time to move to the new key format.
# Any output means a secret key is in a client variable or in the build.
grep -rlE "(NEXT_PUBLIC_|VITE_|EXPO_PUBLIC_)[A-Z0-9_]*(SECRET|SERVICE)" src/ app/ 2>/dev/null
grep -rlE "sb_secret_|service_role" .next/static/ dist/ build/ 2>/dev/nullHow do I ask Copilot for a migration with RLS?
Ask for the table, the RLS statement and the policies together. Name the role and the owner column so Copilot does not fall back to a policy that allows everything.
create table public.messages (
id bigint generated always as identity primary key,
sender_id uuid not null references auth.users (id) on delete cascade,
body text not null,
created_at timestamptz not null default now()
);
alter table public.messages enable row level security;
create policy "messages: sender reads" on public.messages
for select to authenticated
using ((select auth.uid()) = sender_id);
create policy "messages: sender inserts" on public.messages
for insert to authenticated
with check ((select auth.uid()) = sender_id);
create index messages_sender_idx on public.messages (sender_id);How do I check Copilot's policies before they reach production?
- Run the query below and read every policy that allows all rows. Keep only the ones you meant, such as public read-only content.
- Run the RLS-off query and expect zero rows.
- Open Security Advisor and clear lints 0013, 0008, 0010 and 0024.
-- Policies that allow every row.
select tablename, policyname, cmd, roles
from pg_policies
where schemaname = 'public'
and (qual = 'true' or with_check = 'true');-- Every table in public without RLS. Expect zero rows.
select c.relname
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'public' and c.relkind = 'r' and not c.relrowsecurity;Frequently asked questions
- Do VS Code and the Copilot CLI share one Supabase MCP setup?
- No. The IDE reads its own MCP config, such as .vscode/mcp.json, and the CLI reads ~/.copilot/mcp-config.json. Set project scoping and read-only mode in each.
- Can I use Supabase's AI prompts with Copilot?
- Supabase's AI prompts page names GitHub Copilot. Use those prompts as a starting point, and still review every policy Copilot writes before you apply it.
- Which check should I run last before deploying a Copilot project?
- Grep the production build for sb_secret_ and service_role and expect no output. Then run the RLS-off query against production and expect zero rows.