supabase-js code on Sovaria
Run supabase-js, -py, -swift, -kt and -dart code on Sovaria: what works unchanged (tables, rpc, auth, MFA, admin, storage, realtime, functions), typed clients, and the few deliberate differences.
Sovaria's SDKs speak supabase's API. After you move the data (Migrate from Supabase or Firebase), most app code only needs a new client: the same table queries, auth calls, storage buckets, channels and function calls run against your Sovaria project, under your project's policies and row-level security. This page lists what is covered, call by call, and the handful of places where Sovaria behaves differently on purpose.
#The client
Use the project URL and the anon key from the console's Connect panel. In TypeScript, results: 'supabase' makes every awaited call resolve to { data, error } and never reject, as supabase-js does; without it, calls resolve to the rows and throw a SovariaError (Sovaria's own style). Keep your variable name — nothing else in the file has to change. The other SDKs already return values and raise or throw on errors, exactly like their supabase counterparts.
import { createClient } from '@sovaria/sdk'
import type { Database } from './database.types' // sov gen types typescript > src/database.types.ts
export const supabase = createClient<Database>(import.meta.env.VITE_SOVARIA_URL, import.meta.env.VITE_SOVARIA_ANON_KEY, {
results: 'supabase',
auth: { persistSession: true, storageKey: 'my-app-auth' }, // supabase-js's auth options are read too
})#Tables, rpc and types
Everything on from(table) works as in supabase-js: select with related rows (author:authors(name), !hint, !inner), every filter (eq(col, null) is IS NULL), or, not, textSearch, order / limit / range (also on embedded rows with referencedTable), single / maybeSingle, select(cols, { count, head }), insert / upsert / update / delete with .select(), and throwOnError(). rpc(fn, args, { head, get, count }) takes the same filters on a function that returns rows.
const { data: books, count, error } = await supabase
.from('books').select('id,title,author:authors(name)', { count: 'exact' })
.eq('genre', 'sci-fi').order('title').range(0, 19)
const { data: club } = await supabase.from('clubs').insert({ name: 'Readers' }).select().single()
const { data: top } = await supabase.rpc('top_books', { days: 7 }).eq('genre', 'sci-fi').limit(5)sov gen types typescript writes supabase's Database type — Tables.<t>.Row / Insert / Update, Views, Functions (Args / Returns) and Enums, plus the Tables<'t'>, TablesInsert, TablesUpdate and Enums helpers — so createClient<Database>() types every from() and rpc() the same way. Identity, defaulted and nullable columns are optional on insert; generated columns can't be written. sov gen types python | swift | kotlin | dart writes models for the other SDKs (CLI reference).
#Auth
| supabase | Sovaria | Notes |
|---|---|---|
signUp({ email, password, options: { data } }) | the same | data becomes user_metadata. With email confirmation on, session is null until the user confirms. |
signInWithPassword, signInWithOtp, verifyOtp, signInWithOAuth, signInAnonymously | the same | OAuth comes back with ?code=…; exchangeCodeForSession(code) (or handleEmailLink()) finishes it, with PKCE. |
getUser, updateUser, getSession, setSession, refreshSession, signOut | the same | getSession() resolves { data: { session } } in supabase mode. |
onAuthStateChange(cb) → data.subscription.unsubscribe() | the same | Events: INITIAL_SESSION, SIGNED_IN, TOKEN_REFRESHED, USER_UPDATED, SIGNED_OUT. |
resetPasswordForEmail, reauthenticate | the same | The reset link lands on your page with type=recovery; finish with confirmPasswordReset(token, newPassword). |
linkIdentity({ provider }), unlinkIdentity(identity), getUserIdentities() | the same | Linking is bound to this browser by a PKCE verifier; the last sign-in method can't be removed. |
mfa.enroll / challenge / verify / challengeAndVerify / unenroll / listFactors / getAuthenticatorAssuranceLevel | the same | TOTP, phone and security-key factors (Multi-factor authentication). |
auth.admin.* with the service_role key | auth.admin.* with an org owner/admin session | See below and Admin API (auth.admin). |
const { data, error } = await supabase.auth.signInWithPassword({ email, password })
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => render(session))
const { data: factor } = await supabase.auth.mfa.enroll({ factorType: 'totp' }) // show factor.totp.qr_code
await supabase.auth.mfa.challengeAndVerify({ factorId: factor.id, code })#Storage
storage.from(bucket) has supabase's calls: upload (upsert, contentType, cacheControl), update, download ({ transform }), remove([…]), list, move, copy, info, exists, getPublicUrl(path, { download, transform }), createSignedUrl(s) ({ download, transform }), createSignedUploadUrl and uploadToSignedUrl. Bucket admin: createBucket(id, { public, fileSizeLimit, allowedMimeTypes }), getBucket, listBuckets, updateBucket, deleteBucket, emptyBucket (project admins). Image transforms run on paid plans (Image transformations); bucket rules decide every call (Access, links and delivery).
const avatars = supabase.storage.from('avatars')
await avatars.upload(`${user.id}/me.png`, file, { upsert: true, cacheControl: '3600' })
const { data } = avatars.getPublicUrl(`${user.id}/me.png`, { transform: { width: 96, height: 96 } })
await avatars.copy(`${user.id}/me.png`, `${user.id}/me-2025.png`)#Realtime
channel(name).on('broadcast' | 'presence' | 'postgres_changes', filter, callback).subscribe() works with send, track, untrack, presenceState(), unsubscribe(), removeChannel() and removeAllChannels() (Broadcast, presence and table changes).
const room = supabase.channel('room-1')
.on('broadcast', { event: 'cursor' }, ({ payload }) => moveCursor(payload))
.on('presence', { event: 'sync' }, () => setOnline(room.presenceState()))
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages' }, () => reload())
.subscribe()#Functions
functions.invoke(name, { body, headers, method }) resolves { data, error } in supabase mode, with FunctionsHttpError (your function answered 4xx/5xx; error.context is the Response), FunctionsRelayError (the platform refused the call: policy, plan limit, quota) and FunctionsFetchError (no answer). It calls the function the way an app does — the user's token (or none, for public functions), the function's own access policy and your plan's limits. Deno serve() functions deploy as HTTP functions (HTTP functions).
const { data, error } = await supabase.functions.invoke('send-invite', { body: { email } })
if (error?.name === 'FunctionsHttpError') console.log(await error.context.json())#Deliberate differences
| What | On Sovaria | Why |
|---|---|---|
postgres_changes payloads | new and old are {}; you get the table, eventType, commit_timestamp and row_count. Re-read with your own query when the callback fires. filter is accepted but doesn't narrow the events. | Broadcasting the row would show it to every subscriber, whatever row-level security says. Re-reading goes through the rules. |
| Admin API key | auth.admin needs an org owner/admin session (a team member's token or a server key), never a key that ships in an app. | There is no all-powerful key in a browser bundle to leak; admin calls are audited and rate-limited. |
functions.invoke in Sovaria's own mode | The team's management call (runs a function as a team member). Apps use call() or the supabase mode. | Keeps the operator route and the app route apart, each with its own checks. |
| Presence keys | Each presence entry also carries user_id, stamped by the server. | A client can choose a presence key, but not who it is. |
generateLink | Returns magiclink, recovery and invite links (not signup); at most 60 an hour per admin, audited. | A link signs that user in: it is treated like a password. |
| Storage object storage name on mobile | sovaria.files.from(…) | sovaria.storage holds the session there. |
#Checklist
Move the data and users
sov import supabase(Migrate from Supabase or Firebase): tables, RLS policies (auth.uid()works), users with their passwords, storage files.Swap the client
The project URL and anon key;
results: 'supabase'in TypeScript. Generate types withsov gen types.Fix the differences
Callbacks that read
payload.new: re-read instead. Server code that used the service_role key: a team member's token or a server key, never in the app.Test the rules
Sign in as two users and an anonymous visitor; Simulate in the console shows who can do what (Testing your app).