Skip to content

Supabase integration

For credential creation, test accounts, production accounts and environment-isolated execution, follow the account setup guide first. The direct-constructor examples below also work outside the managed launcher; use the launcher when you need its separation guarantees.

ZeoCore's Supabase integration covers the application SDK surface: Database, Auth, Storage, Edge Functions, and Realtime. It does not administer a Supabase project and it does not expose Vault plaintext.

1. Create a project and obtain the public configuration

  1. Sign in to Supabase and choose New project.
  2. Choose the owning organization, a project name, region, and strong database password. Store the password in a password manager; ZeoCore does not need it for Data API calls.
  3. Wait until the project is ready, then open Connect → App Frameworks.
  4. Copy the Project URL and Publishable key. If the project predates publishable keys, create one under Settings → API Keys. Do not copy the legacy JWT-looking anon key into new instructions.
  5. Install ZeoCore and create a local environment file:
uv add "zeocore[supabase]"
cp .env.example .env

Add the values shown under Connect → App Frameworks in the Supabase dashboard:

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_PUBLISHABLE_KEY=sb_publishable_...

Use a publishable key for user-facing applications. Row Level Security (RLS) is the authority. A broader project grant never widens a ZeoCore operation by itself.

SUPABASE_SECRET_KEY is server-only. ZeoCore ignores it unless non-secret configuration explicitly sets allow_privileged_key: true. That is suitable only for an isolated broker or trusted maintenance process—not a browser, model tool, preview deployment, or ordinary web runtime.

The URL identifies the project. The publishable key identifies this application component; it is not a user identity and it is not a database permission grant. Postgres grants and RLS still decide which operations and rows are reachable.

2. Create a least-privilege teaching table

For a first read, open SQL Editor in the dashboard and run the following as a one-off learning setup. In a real application, commit equivalent SQL as a versioned migration and test both allowed and denied cases.

create table public.zeocore_demo_tasks (
    id bigint generated by default as identity primary key,
    title text not null,
    status text not null check (status in ('ready', 'done'))
);

alter table public.zeocore_demo_tasks enable row level security;
revoke all on table public.zeocore_demo_tasks from anon, authenticated;
grant select on table public.zeocore_demo_tasks to anon;

create policy "public may read the teaching rows"
on public.zeocore_demo_tasks
for select
to anon
using (true);

insert into public.zeocore_demo_tasks (title, status)
values ('Connect ZeoCore safely', 'ready');

This deliberately grants anonymous read access to one teaching table and no write access. Do not copy that policy to private data. A signed-in application would normally grant authenticated only and use auth.uid() in each policy. Supabase checks table privileges first, then adds the RLS policy as a row filter.

Run the bounded example:

SUPABASE_DEMO_TABLE=zeocore_demo_tasks \
  uv run --env-file .env python examples/supabase_usage.py

Without SUPABASE_DEMO_TABLE, the example performs no network request. With it, the code loads .env, validates the exact project origin and publishable-key class, initializes the official Python SDK, selects at most five rows from the named table, and prints the normalized result. It never sends SQL.

3. Initialize once

from zeo_core.config import load_dotenv_file
from zeo_core.integrations.database.supabase import SupabaseIntegration

load_dotenv_file()
supabase = SupabaseIntegration()
result = supabase.initialize()
if not result.success:
    raise RuntimeError(result.error)

Initialization creates no table, bucket, user, or network request. It constructs the official SDK client with bounded timeouts and in-memory session persistence disabled by default.

4. Database

from zeo_core.integrations.database.supabase import (
    SupabaseFilter,
    SupabaseFilterOperator,
    SupabaseOrder,
)

ready = supabase.select(
    "tasks",
    columns=("id", "title", "created_at"),
    filters=(
        SupabaseFilter(
            field="status",
            operator=SupabaseFilterOperator.EQ,
            value="ready",
        ),
    ),
    orders=(SupabaseOrder(field="created_at", descending=True),),
    limit=25,
    count="exact",
)

created = supabase.insert("tasks", {"title": "Make tea", "status": "ready"})
changed = supabase.update(
    "tasks",
    {"status": "done"},
    filters=(SupabaseFilter(field="id", value=created.content.rows[0]["id"]),),
)

update() and delete() reject an empty filter list. Table, column, function, schema, and filter-field names are identifiers—not fragments of SQL. For transactional domain behavior, publish a reviewed PostgreSQL function and call it by name with rpc(). ZeoCore never accepts raw SQL.

5. Auth

signed_in = supabase.sign_in_with_password(
    email="member@example.com",
    password=password_from_a_secret_input,
)
print(signed_in.content.user.email)

Passwords and Supabase access/refresh tokens cross the SDK boundary because the provider requires them, but they are never returned in SupabaseSessionStatus or IntegrationResult. The public result contains only authenticated, safe user identity/metadata, and expires_at.

Available flows include password sign-up/sign-in, email OTP, OAuth initiation, verified user lookup, explicit refresh, password-reset email, and sign-out. Persist rotating sessions in your application-owned custody layer, not in a model prompt or log.

6. Storage

supabase.upload_bytes(
    "artifacts",
    "reports/run-42.json",
    report_bytes,
    content_type="application/json",
)
downloaded = supabase.download_bytes("artifacts", "reports/run-42.json")

Object paths are relative POSIX paths and reject traversal. Uploads and downloads are limited by max_object_bytes. Bucket listing/creation/deletion, object listing, copy, move, and bounded removal are available. Signed URLs are not: they are bearer credentials and need a product-specific disclosure and expiry contract.

7. Edge Functions

invoked = supabase.invoke_function(
    "render-report",
    body={"report_id": "42"},
)

The caller supplies a function name, never a URL. Authorization, apikey, and Cookie headers are rejected because credential injection belongs to the SDK. Responses are bounded and normalized without raw response headers.

8. Realtime

Realtime is async-only in Supabase's Python SDK, so ZeoCore does not hide an event loop:

from zeo_core.integrations.database.supabase import SupabaseRealtimeEvent

subscription = await supabase.realtime.subscribe_table(
    "tasks",
    on_change,
    event=SupabaseRealtimeEvent.UPDATE,
)
try:
    await wait_for_your_application_signal()
finally:
    await supabase.realtime.unsubscribe(subscription.id)
    await supabase.realtime.close()

Reconnect and retry policy stays with the host application so a connection failure cannot silently multiply work.

9. Vault and privileged roles

There is intentionally no vault() or decrypted_secrets() method. Supabase Vault decrypts secrets when its decrypted view is queried; an application role that can select that view can obtain plaintext. A safe hosted custody design uses separate web and broker identities, restrictive function privileges, fixed search_path, authenticated metadata, encrypted envelopes, audit evidence, and preview isolation.

ZeoCore supplies the integration boundary. Your migrations and deployment must prove RLS, Storage policies, function grants, role separation, backup behavior, and cross-tenant refusal against the real project.

10. Deployment profiles

  • Local teaching: .env contains only the project URL and publishable key. The example is read-only and RLS is the authority.
  • User-facing application: use the publishable key plus a verified Supabase Auth session. Persist refresh material in application custody, not logs or model context.
  • Broker or maintenance process: opt into a secret key only in an isolated server runtime with its own authorization checks. Secret keys bypass RLS.
  • Preview: use a separate project and synthetic data. A preview must not receive production keys, database credentials, Vault access, or customer artifacts.

Before production, verify grants and RLS for every exposed table, configure Storage policies, restrict function execution, set resource limits, exercise backup/restore, and prove that a second tenant cannot read or mutate the first.

Official references