> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.hoop.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Supabase

> Guardrails, data masking and audit in front of a Supabase Postgres database, with no app code change.

The Sidecar runs in your infrastructure, between your Postgres clients and the Supabase connection pooler.

Clients change only the host and port of their connection string. Every statement from an AI agent, from a developer in `psql`, or from an application then passes through your guardrails, masking rules and audit trail.

![AI agents, developers in psql, and applications connect to the Hoop Sidecar in your infrastructure. The Sidecar applies guardrails, data masking and audit, then connects over verified TLS to the Supabase session pooler and Postgres. Network Restrictions allow only the Sidecar's IP. supabase-js reaches the Supabase HTTPS APIs and does not pass through the Sidecar.](https://mintcdn.com/hoopdev/DDJ2QVgv-XfmMWPp/images/integrations/supabase-sidecar-light.svg?fit=max\&auto=format\&n=DDJ2QVgv-XfmMWPp\&q=85\&s=f26394290f0da4bdef5c34e3e5e717e5)

> [!WARNING]
>
> The Sidecar sees Postgres connections only. Supabase's HTTPS APIs (PostgREST and `supabase-js`, Storage, Auth, Realtime) and the dashboard SQL editor reach the database inside Supabase, so their traffic does not pass through the Sidecar.

***

## Requirements

* A Supabase project. The free plan works.
* Docker on a host with outbound IPv4 access.
* A Postgres client, such as `psql`.

***

## Setup

**1. Copy the session pooler host**

In the Supabase dashboard, click **Connect**, open the **Direct** tab and select **Session pooler**.

![Supabase Connect dialog with Session pooler selected](https://mintcdn.com/hoopdev/szdS_UoMUNauQPss/images/integrations/supabase-connect-session-pooler.png?fit=max\&auto=format\&n=szdS_UoMUNauQPss\&q=85\&s=7e075a6718d2461c851ab8253c941535)

The URI below the options holds the two values you need:

```
postgresql://postgres.<project-ref>:[YOUR-PASSWORD]@aws-0-<region>.pooler.supabase.com:5432/postgres
```

* **Host:** `aws-0-<region>.pooler.supabase.com`. Copy it exactly, because some projects use `aws-1`.
* **User:** `postgres.<project-ref>`.

| Connection method | Use with the Sidecar |
| - | - |
| Session pooler, port `5432` | **Yes.** IPv4, and one server connection per client, like a direct connection. |
| Direct connection | Only from a host with IPv6 egress, or with the IPv4 add-on. It is IPv6 only by default. |
| Transaction pooler, port `6543` | No. It does not support prepared statements, which most drivers and ORMs use. |

**2. Download the Supabase root certificate**

Open **Database**, then **Settings**. In **SSL configuration**, click **Download certificate** and save the file as `certs/supabase-root.crt`.

![Supabase SSL configuration with the Download certificate button](https://mintcdn.com/hoopdev/szdS_UoMUNauQPss/images/integrations/supabase-ssl-certificate.png?fit=max\&auto=format\&n=szdS_UoMUNauQPss\&q=85\&s=1971684c4b3806961fc01f046f7a6aa1)

The pooler presents a certificate signed by Supabase's own CA (`Supabase Root 2021 CA`). That CA is not in public trust stores, so the Sidecar needs this file to verify the pooler.

**3. Write the config**

Save this as `config.yaml` next to the `certs` folder. Replace the `upstream` host with yours.

```yaml config.yaml theme={"dark"}
admin:
  listen: 0.0.0.0:19000

audit:
  file: "-"
  query_sessions: 500

guardrails:
  rules:
    - name: no-destructive-sql
      type: operation
      operations: [drop, delete, truncate]
      message: destructive statements are blocked by Hoop

mask:
  rules:
    - {name: emails, entities: [EMAIL_ADDRESS], strategy: redact}

listeners:
  - name: supabase
    protocol: postgres
    listen: 0.0.0.0:15432
    upstream: aws-0-<region>.pooler.supabase.com:5432
    upstream_tls:
      ca_file: /etc/hoop-inspect/certs/supabase-root.crt
```

* `guardrails` blocks `DROP`, `DELETE` and `TRUNCATE`.
* `mask` replaces email addresses in results.
* `audit` writes every statement to stdout as JSON lines, and `query_sessions` turns on the `/api/sessions` endpoint.
* `upstream_tls` encrypts the hop to Supabase and verifies it against the root certificate.
* `admin` serves health checks and the audit API. It has no authentication, so the next step publishes it on loopback only.

The config holds no database password. The Sidecar relays the login from the client to Supabase.

**4. Validate and start the Sidecar**

```bash theme={"dark"}
docker run --rm \
  -v "$PWD/config.yaml:/etc/hoop-inspect/config.yaml:ro" \
  -v "$PWD/certs:/etc/hoop-inspect/certs:ro" \
  hoophq/hoopsidecar:<version> hoop start sidecar --validate

docker run -d --name hoop-supabase \
  -p 127.0.0.1:15432:15432 -p 127.0.0.1:19000:19000 \
  -v "$PWD/config.yaml:/etc/hoop-inspect/config.yaml:ro" \
  -v "$PWD/certs:/etc/hoop-inspect/certs:ro" \
  hoophq/hoopsidecar:<version>
```

`--validate` prints `config OK: 1 listener(s)` and the rule counts. Versions are on the [releases page](https://github.com/hoophq/hoop/releases).

**5. Point your clients at the Sidecar**

Change the host and port. The user, password and database stay the same. Every client takes the same change: the MCP server or `psql` an AI agent uses, a developer's `psql`, and application drivers.

```
before: postgresql://postgres.<project-ref>@aws-0-<region>.pooler.supabase.com:5432/postgres
after:  postgresql://postgres.<project-ref>@127.0.0.1:15432/postgres?sslmode=disable
```

`sslmode=disable` applies only to the hop from the client to the Sidecar on the same host. When clients reach the Sidecar over a network, turn on [`downstream_tls`](https://mintlify.hoop.dev/docs/setup/configuration/hoop-sidecar/protocols/postgres#tls-on-each-leg) and connect with `sslmode=require`.

***

## Verify

Create a test table through the Sidecar:

```bash theme={"dark"}
psql "postgresql://postgres.<project-ref>@127.0.0.1:15432/postgres?sslmode=disable"
```

```sql theme={"dark"}
create table customers (id serial primary key, name text, email text);
insert into customers (name, email) values ('Ana Souza', 'ana.souza@example.com');
```

A select returns the email masked:

```
postgres=> select * from customers;
 id |   name    |          email
----+-----------+--------------------------
  1 | Ana Souza | [REDACTED:EMAIL_ADDRESS]
```

A destructive statement is refused with your message:

```
postgres=> drop table customers;
FATAL:  destructive statements are blocked by Hoop
```

The audit lists each session with its principal and counts (fields trimmed):

```bash theme={"dark"}
curl -s 127.0.0.1:19000/api/sessions
```

```json theme={"dark"}
{"sessions":[{"principal":"postgres.<project-ref>","protocol":"postgres","connection":"supabase","denied_count":1,"masked_count":1,"verdict":"denied"}],"total":1}
```

***

## Allow only the Sidecar with Network Restrictions

Supabase Network Restrictions limit which IP addresses can connect to Postgres and the pooler.

Allow only the Sidecar's egress IP, and no client with another IP can connect to Supabase directly and skip your rules. Workloads that share the Sidecar's host or NAT gateway keep that access, so give the Sidecar a static egress IP of its own.

1. Open **Database**, then **Settings**, and find **Network Restrictions**.
2. Click **Add restriction**, then add the Sidecar's IP as `<ip>/32`.

![Supabase Network restrictions with one IPv4 address allowed](https://mintcdn.com/hoopdev/szdS_UoMUNauQPss/images/integrations/supabase-network-restrictions.png?fit=max\&auto=format\&n=szdS_UoMUNauQPss\&q=85\&s=e271ec3e39345d24e73cfba98fb46396)

Or with the Supabase CLI:

```bash theme={"dark"}
supabase network-restrictions update --project-ref <project-ref> --db-allow-cidr <ip>/32 --experimental
```

A connection from any other IP fails before it reaches the database:

```
FATAL:  (EADDRNOTALLOWED) address not in tenant allow_list
```

> [!NOTE]
>
> Network Restrictions cover Postgres only, so Supabase's HTTPS APIs keep working. Edge Functions lose direct database access and must use `supabase-js`.

***

## Name who ran each statement

By default every client connects as `postgres.<project-ref>`, so the audit shows one shared user. Two ways give each statement a name.

**A role per person.** Create a Postgres role for each person and connect as `<role>.<project-ref>`. The audit records that role as the principal.

```bash theme={"dark"}
psql "postgresql://alice.<project-ref>@127.0.0.1:15432/postgres?sslmode=disable"
```

**A tag on the connection.** Send a custom setting in the startup options. The Sidecar records it on every event of the session, and Supabase accepts it.

```bash theme={"dark"}
PGOPTIONS='-c hoop.user=alice' psql "postgresql://postgres.<project-ref>@127.0.0.1:15432/postgres?sslmode=disable"
```

```json theme={"dark"}
{"principal":"postgres.<project-ref>","metadata":{"postgres.option.hoop.user":"alice"}}
```

See [trace ids from the startup packet](https://mintlify.hoop.dev/docs/setup/configuration/hoop-sidecar/protocols/postgres.md#trace-ids-from-the-startup-packet) to choose which options the Sidecar records.

> [!WARNING]
>
> The audit records the full statement text. Run `CREATE ROLE ... PASSWORD` from the Supabase SQL editor, so the password does not go through the Sidecar.

***

## Behavior to know

* **A denied statement closes the connection.** The client reads the message as `FATAL` `42501`, and Postgres rolls back the open transaction. Connection pools open a new connection. See [denials the user can read](https://mintlify.hoop.dev/docs/core-concepts/sidecar.md#denials-the-user-can-read).
* **Supabase errors reach the client unchanged.** A login or Network Restrictions failure reads the same with or without the Sidecar.
* **The Sidecar removes channel binding.** It offers `SCRAM-SHA-256` instead of `SCRAM-SHA-256-PLUS`, with the same password. See [TLS on each leg](https://mintlify.hoop.dev/docs/setup/configuration/hoop-sidecar/protocols/postgres.md#tls-on-each-leg).

***

## Free and Enterprise

> [!NOTE]
>
> **Free tier:** one Data Masking rule and one Guardrail per Sidecar are free, forever. Running more than one rule per feature, or managing rules centrally across Sidecars, requires [Enterprise](https://hoop.dev/start).

* **More rules:** an Enterprise license with the guardrails and data-masking features lifts the one-rule caps, with or without a Control Plane. In Docker, pass it with `-e HOOP_LICENSE="$(cat license.json)"`. See [Licensing](https://mintlify.hoop.dev/docs/setup/configuration/hoop-sidecar/config-file.md#licensing).
* **Reviews and central management:** connect the Sidecar to a [Control Plane](https://mintlify.hoop.dev/docs/core-concepts/control-plane.md), which also needs an Enterprise license. It adds reviews, shared rule sets and one view of many Sidecars.

***

## Next

> [card] [Guardrails](https://mintlify.hoop.dev/docs/features/guardrails.md)
>
> Write rules that block statements by operation, table or content.

> [card] [Data Masking](https://mintlify.hoop.dev/docs/features/data-masking.md)
>
> Choose entity types, columns and masking strategies.

> [card] [PostgreSQL](https://mintlify.hoop.dev/docs/setup/configuration/hoop-sidecar/protocols/postgres.md)
>
> What the Postgres lane decodes, terminates and masks.

> [card] [Config File Reference](https://mintlify.hoop.dev/docs/setup/configuration/hoop-sidecar/config-file.md)
>
> Every section and field of `config.yaml`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.