# supabase-js code on Sovaria
Source: https://sovaria.in/docs/supabase-to-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](https://sovaria.in/docs/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.

(Diagram: One client swap; the rest of the app stays)

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

TypeScript:
```ts
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
})
```

Python:
```python
from sovaria import create_client

async with create_client(SOVARIA_URL, SOVARIA_ANON_KEY) as supabase:
    res = await supabase.from_("todos").select("*").eq("done", False).execute()
```

Swift:
```swift
import Sovaria

let supabase = Sovaria(url: sovariaURL, anonKey: anonKey)
let todos = try await supabase.from("todos").select().eq("done", false).execute()
```

Kotlin:
```kotlin
val supabase = Sovaria(SOVARIA_URL, SOVARIA_ANON_KEY)
val todos = supabase.from("todos").select().eq("done", false).execute()
```

Dart:
```dart
final supabase = Sovaria(url: sovariaUrl, anonKey: anonKey);
final todos = await supabase.from('todos').select().eq('done', false);
```

> **Note:** Swift, Kotlin and Dart keep the session in `sovaria.storage` (a `SessionStorage`), so object storage is `sovaria.files`: write `supabase.files.from('avatars')` where your code had `supabase.storage.from('avatars')`.

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

TypeScript:
```ts
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)
```

Python:
```python
books = await supabase.from_("books").select("id,title,author:authors(name)").eq("genre", "sci-fi").order("title").limit(20)
top = await supabase.rpc("top_books", {"days": 7}).eq("genre", "sci-fi").limit(5)
```

Swift:
```swift
let books = try await supabase.from("books").select("id,title,author:authors(name)")
  .eq("genre", "sci-fi").order("title").limit(20).execute()
let top = try await supabase.db.rpc("top_books", args: ["days": 7])
```

Kotlin:
```kotlin
val books = supabase.from("books").select("id,title,author:authors(name)")
  .eq("genre", "sci-fi").order("title").limit(20).execute()
val top = supabase.db.rpc("top_books", mapOf("days" to 7))
```

Dart:
```dart
final books = await supabase.from('books').select('id,title,author:authors(name)')
    .eq('genre', 'sci-fi').order('title').limit(20);
final top = await supabase.db.rpc('top_books', args: {'days': 7});
```

`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](https://sovaria.in/docs/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](https://sovaria.in/docs/mfa)). |
| `auth.admin.*` with the service_role key | `auth.admin.*` with an org owner/admin session | See below and [Admin API (auth.admin)](https://sovaria.in/docs/auth-admin-api). |

TypeScript:
```ts
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 })
```

Python:
```python
res = await supabase.auth.sign_in_with_password({"email": email, "password": password})
factor = await supabase.auth.mfa.enroll({"factor_type": "totp"})
await supabase.auth.mfa.challenge_and_verify({"factor_id": factor["id"], "code": code})
```

Swift:
```swift
let session = try await supabase.auth.signInWithPassword(email: email, password: password)
let factor = try await supabase.auth.mfa.enroll(factorType: "totp")
try await supabase.auth.mfa.challengeAndVerify(factorId: factor["id"] as! String, code: code)
```

Kotlin:
```kotlin
val session = supabase.auth.signInWithPassword(email, password)
val factor = supabase.auth.mfa.enroll("totp")
supabase.auth.mfa.challengeAndVerify(factor["id"].toString(), code)
```

Dart:
```dart
final session = await supabase.auth.signInWithPassword(email: email, password: password);
final factor = await supabase.auth.mfa.enroll();
await supabase.auth.mfa.challengeAndVerify(factorId: '${factor['id']}', code: 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](https://sovaria.in/docs/storage-image-transforms)); bucket rules decide every call ([Access, links and delivery](https://sovaria.in/docs/storage-access)).

TypeScript:
```ts
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`)
```

Python:
```python
avatars = supabase.storage.from_("avatars")
await avatars.upload(f"{user_id}/me.png", png, {"content-type": "image/png", "upsert": "true", "cache-control": "3600"})
url = avatars.get_public_url(f"{user_id}/me.png", {"transform": {"width": 96, "height": 96}})
```

Swift:
```swift
let avatars = supabase.files.from("avatars")
try await avatars.upload("\(userId)/me.png", data: png, contentType: "image/png", upsert: true, cacheControl: "3600")
let url = try avatars.getPublicURL(path: "\(userId)/me.png", transform: TransformOptions(width: 96, height: 96))
```

Kotlin:
```kotlin
val avatars = supabase.files.from("avatars")
avatars.upload("$userId/me.png", png, "image/png", upsert = true, cacheControl = "3600")
val url = avatars.publicUrl("$userId/me.png", transform = TransformOptions(width = 96, height = 96))
```

Dart:
```dart
final avatars = supabase.files.from('avatars');
await avatars.upload('$userId/me.png', png, contentType: 'image/png', upsert: true, cacheControl: '3600');
final url = avatars.getPublicUrl('$userId/me.png', transform: const TransformOptions(width: 96, height: 96));
```

## 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](https://sovaria.in/docs/realtime-broadcast-and-presence)).

TypeScript:
```ts
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()
```

Python:
```python
room = supabase.channel("room-1")
room.on_broadcast("cursor", lambda m: move_cursor(m["payload"]))
room.on_postgres_changes("INSERT", lambda c: reload(), table="messages")
await room.subscribe()
```

Swift:
```swift
let room = supabase.channel("room-1")
room.onBroadcast(event: "cursor") { message in moveCursor(message["payload"]) }
    .onPostgresChange(table: "messages", event: "INSERT") { _ in reload() }
room.subscribe()
```

Kotlin:
```kotlin
val room = supabase.channel("room-1")
room.onBroadcast("cursor") { moveCursor(it["payload"]) }
    .onPostgresChange("messages", "INSERT") { reload() }
    .subscribe()
```

Dart:
```dart
final room = supabase.channel('room-1')
  ..onBroadcast(event: 'cursor', callback: (m) => moveCursor(m['payload']))
  ..onPostgresChanges(table: 'messages', event: 'INSERT', callback: (_) => 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](https://sovaria.in/docs/functions-http)).

TypeScript:
```ts
const { data, error } = await supabase.functions.invoke('send-invite', { body: { email } })
if (error?.name === 'FunctionsHttpError') console.log(await error.context.json())
```

Python:
```python
out = await supabase.functions.invoke("send-invite", invoke_options={"body": {"email": email}})
```

Swift:
```swift
let out = try await supabase.functions.invoke("send-invite", body: ["email": email])
```

Kotlin:
```kotlin
val out = supabase.functions.invoke("send-invite", body = mapOf("email" to email))
```

Dart:
```dart
final out = await supabase.functions.invoke('send-invite', body: {'email': email});
```

## 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

1. **Move the data and users** — `sov import supabase` ([Migrate from Supabase or Firebase](https://sovaria.in/docs/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](https://sovaria.in/docs/testing)).
