supabase

Supabase client SDK for Hot. Query your database (PostgREST), authenticate users (GoTrue), manage file storage, and invoke edge functions.

Installation

Add this to the deps in your hot.hot file:

"hot.dev/supabase": "1.0.0"

Configuration

Set these context variables via the Hot app:

VariableRequiredDescription
supabase.urlYesProject URL (e.g. https://xxxx.supabase.co)
supabase.anon.keyYesAnon/public API key
supabase.service.keyFor admin opsService role key (bypasses RLS)

Find these in your Supabase Dashboard under Settings > API.

Most database, storage object, and edge function helpers use the anon key by default. Use the *-as variants with a user access token for RLS-protected resources, or the *-service variants for trusted server-side service role operations. Storage bucket management uses the service role key.

Usage

Database Queries

::db ::supabase::db
::f ::supabase::filters

// Select with filters
users ::db/select(::db/SelectRequest({
  table: "users",
  select: "id,name,email",
  filters: [::f/gte("age", 18), ::f/eq("active", true)],
  order: "created_at.desc",
  limit: 10
}))

// Select with relations
posts ::db/select(::db/SelectRequest({
  table: "posts",
  select: "title,content,author:users(name,email)"
}))

// Single row
user ::db/select(::db/SelectRequest({
  table: "users",
  filters: [::f/eq("id", "abc-123")],
  single: true
}))

// RLS-aware request as an authenticated user
my-profile ::db/select-as(::db/SelectRequest({
  table: "profiles",
  filters: [::f/eq("id", user-id)],
  single: true
}), session.access_token)

Insert, Update, Delete

::db ::supabase::db
::f ::supabase::filters

// Insert
row ::db/insert(::db/InsertRequest({
  table: "posts",
  data: {title: "Hello", body: "World", author_id: "abc-123"}
}))

// Update
::db/update(::db/UpdateRequest({
  table: "posts",
  data: {title: "Updated Title"},
  filters: [::f/eq("id", row[0].id)]
}))

// Delete
::db/delete(::db/DeleteRequest({
  table: "posts",
  filters: [::f/eq("id", row[0].id)]
}))

// Upsert (insert or update on conflict)
::db/upsert(::db/UpsertRequest({
  table: "products",
  data: [{sku: "X1", name: "Widget", price: 10}],
  on-conflict: "sku"
}))

Stored Procedures (RPC)

::db ::supabase::db

result ::db/rpc(::db/RpcRequest({
  fn-name: "add_them",
  params: {a: 1, b: 2}
}))
// => 3

Filter Helpers

::f ::supabase::filters

::f/eq("status", "active")        // status=eq.active
::f/neq("role", "guest")          // role=neq.guest
::f/gt("age", 21)                 // age=gt.21
::f/gte("age", 18)                // age=gte.18
::f/lt("price", 100)              // price=lt.100
::f/lte("price", 50)              // price=lte.50
::f/like("name", "John*")         // name=like.John*
::f/ilike("email", "*@gmail.com") // email=ilike.*@gmail.com
::f/in_("id", [1, 2, 3])          // id=in.(1,2,3)
::f/is_("deleted_at", "null")     // deleted_at=is.null
::f/not_("status", "eq.archived") // status=not.eq.archived
::f/or_(["age.lt.18", "age.gt.65"])  // or=(age.lt.18,age.gt.65)

Authentication

::auth ::supabase::auth

// Sign up
result ::auth/sign-up(::auth/SignUpRequest({
  email: "alice@example.com",
  password: "securePassword123",
  data: {name: "Alice"}
}))

// Sign in with password
session ::auth/sign-in-password(::auth/SignInPasswordRequest({
  email: "alice@example.com",
  password: "securePassword123"
}))
session.access_token  // use this for authenticated requests

// Get current user
user ::auth/get-user(session.access_token)

// Update user metadata
::auth/update-user(::auth/UpdateUserRequest({
  data: {name: "Alice Smith"}
}), session.access_token)

// Refresh token
new-session ::auth/refresh-token(session.refresh_token)

// Sign out
::auth/sign-out(session.access_token)

// Password recovery
::auth/recover-password("alice@example.com")

Admin Auth (Service Role Key)

::admin ::supabase::auth-admin

// List users
result ::admin/list-users(::admin/ListUsersRequest({page: 1, per_page: 50}))

// Create user (skipping email verification)
user ::admin/create-user(::admin/CreateUserRequest({
  email: "bob@example.com",
  password: "tempPassword123",
  email_confirm: true
}))

// Delete user
::admin/delete-user(user.id)

File Storage

::storage ::supabase::storage

// Upload a file
::storage/upload("avatars", "users/alice.txt", "Hello World", "text/plain")

// Download from private bucket
content ::storage/download("documents", "report.pdf")

// List objects
objects ::storage/list-objects(::storage/ListObjectsRequest({
  bucket: "avatars",
  prefix: "users/",
  limit: 100
}))

// Public URL (no API call)
url ::storage/get-public-url("avatars", "users/alice.jpg")

// Signed URL for temporary access
result ::storage/create-signed-url("documents", "report.pdf", 3600)

// Bucket management
::storage/create-bucket(::storage/CreateBucketRequest({
  name: "uploads",
  public: false,
  file_size_limit: 10485760
}))

Edge Functions

::functions ::supabase::functions

result ::functions/invoke(::functions/InvokeFunctionRequest({
  name: "process-order",
  body: {order_id: "abc-123"},
  access-token: session.access_token
}))

Modules

ModuleDescription
::supabase::dbDatabase CRUD via PostgREST (select, insert, update, delete, upsert, rpc)
::supabase::filtersPostgREST filter helpers (eq, gt, lt, in, like, or, and, etc.)
::supabase::authUser authentication (sign up, sign in, OTP, refresh, password recovery)
::supabase::auth-adminAdmin user management with service role key
::supabase::storageFile storage (buckets, upload, download, signed URLs)
::supabase::functionsEdge function invocation
::supabase::apiLow-level HTTP client with dual-header auth
::supabase::coreProject URL and key helpers

Local Tests

Offline unit tests cover query encoding, PostgREST headers/count parsing, and storage path/header helpers:

cd hot/pkg/supabase
hot test --conf hot.test.hot

Integration Tests

1. Create a Supabase Project

  1. Go to supabase.com and create a project
  2. In Settings > API, copy your Project URL, anon key, and service_role key

2. Create a Test Table

Run this SQL in the Supabase SQL Editor:

CREATE TABLE "hot-integration-test" (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  name text NOT NULL,
  value integer DEFAULT 0,
  created_at timestamptz DEFAULT now()
);

ALTER TABLE "hot-integration-test" ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Allow all for anon" ON "hot-integration-test"
  FOR ALL USING (true) WITH CHECK (true);

CREATE UNIQUE INDEX hot_integration_test_name_idx ON "hot-integration-test" (name);

If PostgREST still reports PGRST205 after creating the table, refresh the schema cache from the Supabase Dashboard or wait briefly and rerun the tests.

3. Create a Test Storage Bucket

The storage object test creates and deletes a temporary bucket by default. Set SUPABASE_TEST_BUCKET only when you want to use an existing bucket instead.

4. Set Context Variables

supabase.url=https://xxxx.supabase.co
supabase.anon.key=eyJ...
supabase.service.key=eyJ...

5. Set Environment Variables

SUPABASE_TEST_TABLE=hot-integration-test
SUPABASE_TEST_BUCKET=hot-integration-test # optional
SUPABASE_TEST_FUNCTION=hello-world

SUPABASE_TEST_TABLE defaults to hot-integration-test. SUPABASE_TEST_BUCKET is optional; when unset, object storage tests create a temporary bucket. SUPABASE_TEST_FUNCTION is optional -- only needed if you have an edge function deployed.

6. Run the Tests

./scripts/integration/supabase.sh

What the Tests Do

Test FileWhat It TestsSide Effects
db.hotInsert, select, update, delete, upsert, filtersCreates/deletes rows in test table
auth.hotSign up, sign in, get user, refresh, sign out, admin listCreates/deletes a test user
storage.hotBucket create/delete, upload, list, signed URL, deleteCreates/deletes a temp bucket + file
functions.hotInvoke edge functionCalls the test function

All tests clean up after themselves.

Documentation

License

Apache-2.0 - see LICENSE