Project Setup and the Supabase CLI
The idea in one sentence
Section titled “The idea in one sentence”A Supabase project starts as a hosted Postgres instance with two API keys of very different trust levels, and the Supabase CLI is what lets you link a local folder to it and run the entire stack on your own machine with Docker.
Creating a project and its two keys
Section titled “Creating a project and its two keys”You create a new hosted project from the Supabase dashboard, or the shortcut database.new. Behind the scenes this provisions a dedicated Postgres instance, an API URL, and two API keys:
anonkey — public, safe to ship in client-side code (a browser bundle, a mobile app). Requests using this key are subject to Row Level Security.service_rolekey — secret, full-access, bypasses Row Level Security entirely. It is meant only for trusted server-side code.
// Server-side only — e.g. an API route, a cron job, an Edge Function.// process.env.SUPABASE_SERVICE_ROLE_KEY must never be bundled into client code.import { createClient } from '@supabase/supabase-js';
const supabaseAdmin = createClient( process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);The Supabase CLI
Section titled “The Supabase CLI”The CLI is how you manage a project as code instead of only through the dashboard:
# Scaffold a local supabase/ config directory in your projectnpx supabase init
# Authenticate the CLI with your Supabase accountsupabase login
# Connect this local project folder to a specific hosted projectsupabase link --project-ref <project-ref>supabase link is what tells your local supabase/ folder which hosted project it corresponds to — every later db push or db diff targets that linked project.
Local development with supabase start
Section titled “Local development with supabase start”supabase startThis spins up the entire Supabase stack locally via Docker — Postgres, Studio, Auth, Storage, Realtime, and the rest, all running as containers on your machine. That means you can develop and test your schema, your RLS policies, and your client code completely offline, with no risk to your hosted project, before ever running supabase db push against it.
Run it (or, if it is already running, supabase status) and the CLI prints every URL and key you need — this is real, trimmed output from this course’s own local stack:
$ supabase status --workdir tools/probe -o envDB_URL="postgresql://postgres:[email protected]:55322/postgres"API_URL="http://127.0.0.1:55321"REST_URL="http://127.0.0.1:55321/rest/v1"GRAPHQL_URL="http://127.0.0.1:55321/graphql/v1"MCP_URL="http://127.0.0.1:55321/mcp"STUDIO_URL="http://127.0.0.1:55323"ANON_KEY="eyJhbGciOiJIUzI1NiIs...I0"SERVICE_ROLE_KEY="eyJhbGciOiJIUzI1NiIs...IU"Every one of these is a real, reachable local endpoint the moment supabase start finishes — DB_URL is a direct Postgres connection, API_URL is Kong’s front door from the previous lesson, and MCP_URL is the subject of the rest of this section.
The Supabase MCP server: supabase start exposes /mcp locally
Section titled “The Supabase MCP server: supabase start exposes /mcp locally”MCP (Model Context Protocol) is the standard AI coding tools (Claude Code, Cursor, and others) use to call tools against an external system instead of only reading and writing files. Supabase ships an official MCP server so an AI agent can inspect your schema, run SQL, check advisors, and generate types directly — without you manually pasting schema dumps into a chat window. Since a recent CLI release, supabase start runs a local MCP server automatically, reachable at the MCP_URL printed above, with no extra setup and no separate npx @supabase/mcp-server-supabase process to manage.
Proof it is a real MCP server, not just an idle URL — a raw JSON-RPC initialize handshake, POSTed directly with curl:
$ curl -s -X POST http://127.0.0.1:55321/mcp \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"fnd-probe","version":"0.1.0"}}}'{"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"supabase","title":"Supabase","version":"0.11.0"},"instructions":"Here are guidelines for using Supabase tools effectively:\n\n- Before making schema changes, use `list_tables`...\n..."},"jsonrpc":"2.0","id":1}A real MCP session, a real server name and version, and even server-authored guidance for the calling agent in instructions. Following up with a tools/list call shows exactly what this local server can do:
$ curl -s -X POST http://127.0.0.1:55321/mcp \ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'search_docs, list_tables, list_extensions, list_migrations, apply_migration,execute_sql, query_logs, get_advisors, get_project_url, get_publishable_keys,generate_typescript_typesHow an AI agent uses it — and the real danger
Section titled “How an AI agent uses it — and the real danger”An AI coding tool connects the same way any MCP client does — for a local stack, pointing straight at the URL above needs no access token at all, since there is no hosted project or PAT in the loop:
{ "mcpServers": { "supabase-local": { "url": "http://127.0.0.1:55321/mcp" } }}Once connected, an agent can call execute_sql to run arbitrary SQL. This is not a hypothetical — proved directly against this local stack:
$ curl -s -X POST http://127.0.0.1:55321/mcp \ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"execute_sql","arguments":{"query":"select current_user, session_user;"}}}'{"current_user":"postgres","session_user":"postgres"}current_user is postgres — the database superuser, not merely service_role. The local MCP server connects to Postgres directly, with full administrative rights: it can read or write any table regardless of RLS, run arbitrary DDL, and inspect any schema, including auth and storage. The official MCP docs put this plainly: “Connecting an LLM to your Supabase projects carries security risks”, naming prompt injection as “the primary attack vector unique to LLMs” — untrusted data returned from a query (a user’s bio field, a support ticket body) could contain text an insufficiently careful agent interprets as a new instruction. The same docs recommend keeping the read_only=true parameter on and manual tool-call approval enabled for exactly this reason, and the self-hosting MCP guide is explicit that the endpoint should never be reachable from the open internet — access only over a VPN or SSH tunnel. Locally this is lower-stakes (it is your own disposable dev database), but the moment the same pattern is pointed at a real project, “give the AI agent the MCP URL” is equivalent to “give the AI agent an unrestricted psql session as the database owner.”
Reading AI-generated code
Section titled “Reading AI-generated code”An AI assistant, asked to “wire up the Supabase MCP server for this project,” produced this config:
🐛 Spot the bug
// An AI assistant's suggested MCP client config, pasted into a shared repo READMEconst mcpConfig = {mcpServers: { supabase: { command: 'npx', args: [ '-y', '@supabase/mcp-server-supabase@latest', '--access-token', 'sbp_<your-personal-access-token>', ], },},};
export {};Show the answer
The bug is not in the JSON shape — it is that a real, live personal access token was committed straight into a config example in a shared repository, exactly the way an AI assistant will happily paste one back if it was given one earlier in the conversation and asked to “write out the config.” A PAT scopes to your whole Supabase account by default (every project, unless --project-ref restricts it, per the tool-permission table in Supabase’s own docs), so a leaked one is not a “this one project” problem — it is an “attacker can list, modify, or delete every project on this account” problem. The fix has two parts: never let a literal token land in a file that reaches version control (reference an environment variable instead — "--access-token", process.env.SUPABASE_PAT in an MCP client that supports env expansion, or the client’s own secrets mechanism), and treat any token an AI assistant has already seen in a conversation as compromised — rotate it from the dashboard, the same way you would after any other accidental secret exposure.
Mental Model
Section titled “Mental Model”The MCP server is not a new way for Supabase to run SQL — it is a translation layer that lets an AI agent call the exact same operations you could run by hand (psql, supabase db push, the Studio SQL editor), through a structured tool-call protocol instead of you copy-pasting. Every capability it exposes maps to something this course already teaches directly: execute_sql is psql -c, apply_migration is supabase migration new plus db push, get_advisors is Studio’s Advisors panel. Treating it as “just another way to run the commands from this course, except an LLM is holding the keyboard” is the right mental model — and the right lens for judging how much trust to extend it.
Trade-offs
Section titled “Trade-offs”- The MCP server lets an AI agent inspect and modify your schema without you hand-writing every SQL statement yourself — at the cost of handing it the same blast radius a human with full database access would have, proved above:
execute_sqlruns as the Postgres superuser locally, not a scoped role. - A hosted personal access token can be scoped to one project (
--project-ref) and made read-only (--read-only) — at the cost of remembering to actually set those flags; the default, unscoped, read-write invocation is the more dangerous one and also the one every copy-pasted example defaults to. - Zero setup locally (no PAT, no
npxwrapper) makes it trivially easy to try — at the cost of zero access control locally either: there is no scoping flag to reach for at all againstMCP_URL, only “point an agent at your local database or don’t.”
Common Misconceptions
Section titled “Common Misconceptions”- “The MCP server only lets an AI agent read data, not change anything.” Disproved by the tool list itself:
apply_migrationandexecute_sqlboth write.get_advisors,list_tables, andsearch_docsare the read-only ones — the danger above is specifically about the tools that are not. - “service_role is the most privileged thing an AI agent could get access to.” The proof above shows the local MCP server runs as
postgres, the actual database superuser — a strictly higher privilege level thanservice_role, which is still just a Postgres role subject to normal grants (service_role simply is not subject to RLS). - “MCP access is safe as long as the AI model itself is trustworthy.” The documented risk is prompt injection from data the query returns, not from the model’s own intentions — a well-behaved model can still be manipulated by text embedded in a row it was asked to read.
flowchart LR
subgraph local["Local machine (supabase start)"]
lpg[("Postgres")]
lstudio["Studio"]
lauth["Auth"]
lstorage["Storage"]
end
subgraph hosted["Hosted project"]
hpg[("Postgres")]
hapi["API URL + anon/service_role keys"]
end
local -- "supabase link --project-ref" --> hosted
local -- "supabase db push" --> hosted