Docs

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.

Updated Oct 31, 2026

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.

unchanged callssame rules as every clientYour app codefrom(), auth, storage,channel(), functions@sovaria/sdk 0.7+createClient(url, key, {results: 'supabase' })Your projectpolicies + row-levelsecurity

#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

supabaseSovariaNotes
signUp({ email, password, options: { data } })the samedata becomes user_metadata. With email confirmation on, session is null until the user confirms.
signInWithPassword, signInWithOtp, verifyOtp, signInWithOAuth, signInAnonymouslythe sameOAuth comes back with ?code=…; exchangeCodeForSession(code) (or handleEmailLink()) finishes it, with PKCE.
getUser, updateUser, getSession, setSession, refreshSession, signOutthe samegetSession() resolves { data: { session } } in supabase mode.
onAuthStateChange(cb) → data.subscription.unsubscribe()the sameEvents: INITIAL_SESSION, SIGNED_IN, TOKEN_REFRESHED, USER_UPDATED, SIGNED_OUT.
resetPasswordForEmail, reauthenticatethe sameThe reset link lands on your page with type=recovery; finish with confirmPasswordReset(token, newPassword).
linkIdentity({ provider }), unlinkIdentity(identity), getUserIdentities()the sameLinking 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 / getAuthenticatorAssuranceLevelthe sameTOTP, phone and security-key factors (Multi-factor authentication).
auth.admin.* with the service_role keyauth.admin.* with an org owner/admin sessionSee 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

WhatOn SovariaWhy
postgres_changes payloadsnew 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 keyauth.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 modeThe 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 keysEach presence entry also carries user_id, stamped by the server.A client can choose a presence key, but not who it is.
generateLinkReturns 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 mobilesovaria.files.from(…)sovaria.storage holds the session there.

#Checklist

  1. 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.

  2. Swap the client

    The project URL and anon key; results: 'supabase' in TypeScript. Generate types with sov gen types.

  3. 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.

  4. Test the rules

    Sign in as two users and an anonymous visitor; Simulate in the console shows who can do what (Testing your app).