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:
| Variable | Required | Description |
|---|---|---|
supabase.url | Yes | Project URL (e.g. https://xxxx.supabase.co) |
supabase.anon.key | Yes | Anon/public API key |
supabase.service.key | For admin ops | Service 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
| Module | Description |
|---|---|
::supabase::db | Database CRUD via PostgREST (select, insert, update, delete, upsert, rpc) |
::supabase::filters | PostgREST filter helpers (eq, gt, lt, in, like, or, and, etc.) |
::supabase::auth | User authentication (sign up, sign in, OTP, refresh, password recovery) |
::supabase::auth-admin | Admin user management with service role key |
::supabase::storage | File storage (buckets, upload, download, signed URLs) |
::supabase::functions | Edge function invocation |
::supabase::api | Low-level HTTP client with dual-header auth |
::supabase::core | Project 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
- Go to supabase.com and create a project
- 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 File | What It Tests | Side Effects |
|---|---|---|
db.hot | Insert, select, update, delete, upsert, filters | Creates/deletes rows in test table |
auth.hot | Sign up, sign in, get user, refresh, sign out, admin list | Creates/deletes a test user |
storage.hot | Bucket create/delete, upload, list, signed URL, delete | Creates/deletes a temp bucket + file |
functions.hot | Invoke edge function | Calls the test function |
All tests clean up after themselves.
Documentation
License
Apache-2.0 - see LICENSE