Migrate from Supabase
vaif migrate --from supabase reads your Supabase project folder and recreates its tables, storage buckets and function stubs in a VAIF Studio project. Data, users, access rules and function code are separate steps, covered below.
What maps to what
Section titled “What maps to what”| Supabase | VAIF Studio | Moved by vaif migrate |
|---|---|---|
Tables (supabase/migrations/*.sql) |
Tables in your project’s database | Yes: tables, columns, primary keys, unique and foreign keys |
Storage buckets ([storage.buckets.*] in config.toml) |
Storage buckets | Yes: the bucket and whether it is public. Not the files |
Edge Functions (supabase/functions/*) |
Functions | A stub for each function. You port the code |
| Access policies written in SQL | Row rules | No. Listed in the plan; you recreate them as row rules |
| Auth users | Your project’s users | No. See Users |
| Email and password, magic links | The same | Turn them on in the console |
| Social sign-in | Social sign-in for your app’s users | No. Create your own OAuth apps and set them up for the project |
| Realtime row changes | Row-change subscriptions | Code change |
@supabase/supabase-js |
@vaifstudio/client |
Code change |
1. Install the CLI and sign in
Section titled “1. Install the CLI and sign in”Install the vaif CLI, then sign in:
npm install -g @vaifstudio/clivaif login2. Preview the migration
Section titled “2. Preview the migration”Run this in your Supabase project’s root, the folder that contains supabase/:
vaif migrate --from supabase --dry-runThe CLI reads supabase/migrations/*.sql, supabase/config.toml and supabase/functions/, prints what it found, and writes the plan to vaif-migration-plan.md: every table and column with its type and constraints, the access policies it found, buckets, functions, sign-in providers and warnings. Nothing is created.
If your schema is not in migration files, export it into one first:
supabase db dump --schema public > supabase/migrations/schema.sqlTo read a folder other than the current one, pass --output-dir <path>.
3. Run it
Section titled “3. Run it”vaif migrate --from supabase --project-id <your-vaif-project-id>The project ID can also come from vaif.config.json. The CLI creates the tables in one schema change, then each bucket, then a stub for each function. Hyphens in function names become underscores, because function names use letters, digits and underscores only. It reports each step and writes the plan file again.
4. Recreate access rules
Section titled “4. Recreate access rules”Supabase policies are SQL expressions such as auth.uid() = user_id. A VAIF row rule is simpler: a column must equal a claim in the user’s access token. The common policy “users see their own rows” becomes a rule with column user_id and claim sub. See Row rules. A policy that cannot be written that way belongs in a function that checks the caller.
For a table in a new SQL file, put the rule above CREATE TABLE:
-- vaif:owner user_idCREATE TABLE notes ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL, body text, updated_at timestamptz DEFAULT now());For a table that is already migrated, create the rule through the API with a console access token:
curl -X POST https://api.vaif.studio/rls/policies/$PROJECT_ID \ -H "Authorization: Bearer $VAIF_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"tableName":"notes","name":"Own notes","operation":"ALL","field":"user_id","matchClaim":"sub"}'Use -- vaif:public-read only when visitors may read every row; it cannot filter, for example, to published rows only. Joins, team membership and admin checks need a function that verifies the user’s token before it uses a secret key. Keep a row rule on those tables too, so they cannot be read directly.
Before you push SQL files, remove Postgres policies, views, SQL functions and triggers. Replace serial ids with uuid PRIMARY KEY DEFAULT gen_random_uuid(), enums with text checked in your code, and composite primary keys with one uuid id plus a unique index on the original columns. Use timestamptz DEFAULT now() for timestamps, and set updated_at in your code or in a function.
5. Copy the data
Section titled “5. Copy the data”Export each table from Supabase, for example with supabase db dump --data-only or a script that uses supabase-js. Then insert the rows from a server with createMany() and a secret key:
import { readFileSync } from "node:fs";import { createVaifClient } from "@vaifstudio/client";
const vaif = createVaifClient({ baseUrl: "https://api.vaif.studio", projectId: process.env.VAIF_PROJECT_ID, apiKey: process.env.VAIF_SECRET_KEY, // vk_srv_: server only});
const rows: Record<string, unknown>[] = JSON.parse(readFileSync("posts.json", "utf8"));
for (let i = 0; i < rows.length; i += 500) { const { count } = await vaif.from("posts").createMany(rows.slice(i, i + 500)); console.log(`inserted ${i + count} of ${rows.length}`);}One createMany() call takes up to 1000 rows, and every row in a call must have the same columns. Load parent tables before the tables whose foreign keys point at them. Copy storage files with upload().
Supabase password hashes cannot be imported. To move your users:
- Create each user with
POST /projects/{projectId}/usersand a body such as{ "email": "...", "metadata": { ... } }, with no password. The call takes a console access token of a project developer or higher. - Ask each user to set a new password: your app calls
vaif.auth.requestPasswordReset({ email }), or the user signs in with a magic link (vaif.auth.magic.send(email)).
Keep the Supabase user id in metadata if other tables point at it, and update those references.
6. Update your code
Section titled “6. Update your code”| supabase-js | @vaifstudio/client |
|---|---|
createClient(url, anonKey) |
createVaifClient({ baseUrl: "https://api.vaif.studio", projectId, apiKey: "vk_pub_..." }) |
.from("t").select("a, b") |
.from("t").select("a", "b").list() |
.order("created_at", { ascending: false }) |
.orderBy("created_at", "desc") |
.insert(row).select() |
.create(row) |
.update(values).eq("id", id) |
.update(id, values) |
.delete().eq("id", id) |
.delete(id) |
.single() |
.single() (returns { data, error }) |
auth.signUp({ email, password }) |
auth.signUp(email, password) |
auth.signInWithPassword({ email, password }) |
auth.login(email, password) |
auth.signInWithOtp({ email }) |
auth.magic.send(email) |
| A channel listening for row changes on a table | from(table).on("*", fn).subscribe() |
functions.invoke(name, { body }) |
functions.invoke(name, { body }) |
storage.from(bucket).upload(path, file) |
upload(vaif, bucket, path, file) from @vaifstudio/client/storage |
A sample after the switch:
import { createVaifClient } from "@vaifstudio/client";
const vaif = createVaifClient({ baseUrl: "https://api.vaif.studio", projectId: process.env.VAIF_PROJECT_ID, apiKey: process.env.VAIF_PUBLISHABLE_KEY,});
type Post = { id: string; title: string; author_id: string; created_at: string };
const { data: latest } = await vaif .from<Post>("posts") .select("id", "title", "created_at") .orderBy("created_at", "desc") .limit(10) .list();
vaif .from<Post>("posts") .on("INSERT", (event) => console.log("new post", event.new?.title)) .subscribe();
console.log(latest);Functions
Section titled “Functions”Supabase Edge Functions run on Deno. VAIF functions run on Node.js by default. The stubs the CLI creates have the same names, with hyphens replaced by underscores. Port each function’s code, and replace Deno-only imports with Node.js built-ins or fetch. See Creating functions.
Checklist
Section titled “Checklist”- Run
vaif migrate --from supabase --dry-runand readvaif-migration-plan.md - Run the migration and check the tables in the console
- Recreate access policies as row rules
- Copy data, parents first, and copy storage files
- Create users and send password resets
- Port functions and switch the app to
@vaifstudio/client