Connect a Supabase Database to Your Agent

Connect a Supabase Database to Your Agent

Supabase is hosted Postgres. Connecting a project gives your agent its own schema inside it. The agent works with that schema in SQL: you describe the data you need and it creates the tables, and from then on every conversation with the agent can read and write rows. Only your own conversations can change the structure.

The project stays yours. Data, billing and the Supabase dashboard are all under your account. Mutiro holds no copy of it.

The Supabase connection is currently enabled per account. Contact us to turn it on for yours.

Connect

  1. Open your agent, go to Connections, and click Connect on the Supabase card.
  2. Authorize Mutiro in the Supabase page that opens and pick the organization that holds your project.
  3. Back in Mutiro, select a project from the list, or create a new one. Creating a project happens in your Supabase organization and follows its plan.
  4. Restart the agent. The card shows the project and the agent's schema name, which looks like mutiro_3f9a....

Build the data by talking

In your own conversation, tell the agent what you want to keep track of:

I run a small clinic. Keep patients and their appointments, with a status for each appointment.

The agent creates the tables in its schema and tells you what it made. Ask it to show the tables, add a column, or drop something it got wrong. Then describe how the data should be used in the agent's instructions or a skill, and share the agent.

People you share with can add, look up and change rows. They cannot alter tables: that tool is not present in their conversations, and the database role they run under refuses schema changes.

Who can change what

Users never touch the database. Nobody gets a login or a connection string. Every read and write goes through the agent, under a role you control.

The rule that matters: the database is the enforcement, instructions are the behavior. An instruction like "never delete a patient, archive instead" shapes what the agent does on a normal day, and the agent can refuse, ask, or explain. It is not a guard. A user who wants to get around it can, given enough persistence, talk the agent into it. The one thing nobody can talk the agent into is a permission its database role does not have.

So for anything that must hold no matter what is said, put it in Postgres:

  • Read-only where nothing should change: grant SELECT alone and the agent cannot write that table for anyone.
  • Constraints and triggers for what a row may look like and which transitions are allowed.
  • Views that expose only the columns or rows an app should see, with grants on the view instead of the table.

The agent cannot change the structure, so those lines stay where you put them. Use instructions for how the agent should behave, and the database for what it is allowed to do.

This fits shared data for a defined group: a clinic's schedule, a team's task board, a course roster. Everyone you share with is meant to see the same data. Per-user isolation, where each person sees only their own rows, is the next step.

Give the agent access to tables you already have

The agent's schema is separate from your application's tables. To let it use existing tables, grant its data role access from the Supabase SQL editor. The role name is the schema name on the connection card followed by _data.

Read and write on everything in public, including tables created later:

GRANT USAGE ON SCHEMA public TO "mutiro_<hash>_data"; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "mutiro_<hash>_data"; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "mutiro_<hash>_data"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "mutiro_<hash>_data"; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO "mutiro_<hash>_data";

For read only, grant SELECT alone. For one table, name it instead of ALL TABLES. Grants apply immediately, no restart needed. REVOKE with the same shape takes access back.

Two things to know before granting:

  • A grant means all rows. The agent's role does not follow your row-level security policies. Anyone who can talk to the agent can reach every row in a granted table.
  • Rows only. The agent can never create, alter or drop tables outside its own schema, whatever you grant.

Multi-tenant tables

If one table holds several customers and row-level security keeps them apart, do not grant the table. The agent's role bypasses those policies and would see every tenant.

For reads, put a view in front of the table that pins one tenant, and grant the view alone. One agent per tenant, each with its own view; the isolation stays in your database and the agent cannot widen it.

CREATE VIEW public.orders_for_acme AS SELECT * FROM public.orders WHERE tenant_id = 'acme'; GRANT USAGE ON SCHEMA public TO "mutiro_<hash>_data"; GRANT SELECT ON public.orders_for_acme TO "mutiro_<hash>_data";

The view runs with its owner's privileges, so the WHERE clause is what the agent sees, not the policy. For writes through a view, add WITH CHECK OPTION so a row cannot be inserted under another tenant. For anything beyond that, the better shape is your application's own API: an edge function or RPC that applies the tenant server-side, called from a custom agent image with tools built for it. The application stays the owner of its isolation and no database credential is involved.

The agent captures, your app ingests

For a large application, writes rarely belong in the agent's hands at all. A shape that keeps your application in control without an API:

  1. Grant the agent tenant-pinned views for what it needs to read.
  2. Let the agent keep its own capture tables in its schema: leads it qualified, orders it took, requests it collected. Describe them in conversation and it creates them.
  3. Run a routine on your side that reads those tables, validates each row against your rules, and inserts into your real tables. Stamp the rows it took rather than deleting them, so the agent can still answer what it submitted and a rejected row stays visible for correction.

The agent never holds a credential that writes your tables. Your routine is the only writer, and it validates on the way in. The agent's tables can gain a column when the owner asks for one, so read them by name and ignore what you do not expect. With one agent per customer, each agent has its own schema, and the schema name on the connection card is the key that maps it to the tenant.

Call your own code: Edge Functions

Some work does not belong in SQL or in the agent: calling another service with its own key, heavier processing. Put it in a Supabase Edge Function in your project and the agent calls it with the supabase_invoke tool:

supabase_invoke({ function: "sync-orders", body: { days: 60 } })

The call carries a project API key you create for the agent, so Mutiro never asks for more than the permissions it has, and the key is yours to rotate or revoke. Two steps, once per project:

  1. In the Supabase dashboard, Project Settings, API Keys, create a secret key and choose the agent's data role as its Postgres role: mutiro_<hash>_data, the schema name on the connection card followed by _data. That role is what makes the function see exactly what supabase_sql sees, and nothing more.

  2. Give the key to the agent as a secret, then restart it:

    mutiro agents secrets set <agent> SUPABASE_FUNCTIONS_KEY sb_secret_...
  3. Give the same key to your function too, so it can tell the agent from any other caller:

    supabase secrets set MUTIRO_FUNCTIONS_KEY=sb_secret_...

Until the agent secret is set the tool answers with these instructions, role name included.

Authenticate the caller in the function. The default verify_jwt = true keeps unauthenticated requests out, but it also lets in your project's public key and any signed-in user of your app. Anything that acts on who asked must first check that the request carries the agent's key: compare the apikey header with MUTIRO_FUNCTIONS_KEY and refuse otherwise. Only then are the caller headers trustworthy: x-mutiro-agent, x-mutiro-user, x-mutiro-role (owner or user) and x-mutiro-conversation. Your function's own secrets (a broker key, a mail provider) live in supabase secrets set as well, never in the agent.

// supabase/functions/sync-orders/index.ts import { createClient } from "npm:@supabase/supabase-js@2"; Deno.serve(async (req) => { const key = req.headers.get("apikey") ?? ""; if (!key || key !== Deno.env.get("MUTIRO_FUNCTIONS_KEY")) { return Response.json({ error: "not the agent" }, { status: 401 }); // public key or app user: refuse } const { days } = await req.json(); const supabase = createClient(Deno.env.get("SUPABASE_URL")!, key); // runs as the agent's data role const { data, error } = await supabase.schema("mutiro_<hash>").from("orders").select("*").gte("created_at", since(days)); if (error) return Response.json({ error: error.message }, { status: 400 }); return Response.json({ orders: data, by: req.headers.get("x-mutiro-user") }); });

To read the agent's tables through the Supabase client, expose its schema in Project Settings, Data API, Exposed schemas, or query your own tables granted to the data role as described above. A function may also connect to Postgres directly with its own credentials; the headers still tell it who asked.

Connections made before this tool existed need one grant so the Data API can run as the agent's role; run it once in the SQL editor (new connections have it):

GRANT "mutiro_<hash>_data" TO authenticator;

A call must answer within 25 seconds and responses are cut at 1 MiB. A function that does longer work starts it and answers at once (a queue row, a background task), and the agent asks for the result in a later call. An HTTP error status is returned to the agent as the function's own message, so answer errors as JSON with an error field. The tool is available to every user of the agent, as supabase_sql is; make it owner-only in the Tools tab when the function does something only the owner should trigger.

Disconnect

Disconnect on the card removes the agent's database roles and Mutiro's authorization. The schema and its data stay in your project. Connecting the same agent to the same project again picks them up where they were.