Switch from Supabase to instancez: CLI and code examples
Move from Supabase to instancez and keep your supabase-js code. See the one-line client change, the YAML that replaces migrations, and the migration flow.
Switching from Supabase to instancez means changing one line in your app: the URL and key you pass to createClient. Your supabase-js calls for auth, tables, RPC and storage stay the same. What changes is everything behind them. The backend becomes one Go binary in front of Postgres, and one instancez.yaml file defines it.
This post shows that switch with real commands and code. We ran the YAML, the CLI commands and the client code below against a local instancez before publishing. The deploy commands come from our docs.
What changes in your app code?
One line. Take a clinic appointments app built on Supabase, the same clinic backend we use in one YAML file for your whole backend. Staff sign in to see today’s appointments at their clinic and to upload referral letters. Patients sign in to a portal that shows their own appointments. Here’s the data code:
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY)
// Staff app: today's appointments for the signed-in user's clinic.
export async function todaysAppointments() {
const start = new Date()
start.setHours(0, 0, 0, 0)
const end = new Date(start)
end.setDate(end.getDate() + 1)
return supabase
.from('appointments')
.select('id, patient_id, starts_at, status, patients(full_name), staff(full_name)')
.gte('starts_at', start.toISOString())
.lt('starts_at', end.toISOString())
.order('starts_at')
}
// Staff app: attach a referral letter to a patient.
export async function uploadReferral(patientId, file) {
const { data: clinicId, error } = await supabase.rpc('my_clinic')
if (error) return { error }
return supabase.storage
.from('documents')
.upload(`${clinicId}/${patientId}/referral.pdf`, file, { contentType: 'application/pdf' })
}
// Patient portal: RLS returns only this patient's appointments.
export async function myAppointments() {
return supabase.from('appointments').select('starts_at, status').order('starts_at')
}
Sign-in stays supabase.auth.signInWithPassword(). To move the app to instancez, you change the values you pass in:
- createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY)
+ createClient(process.env.INSTANCEZ_URL, process.env.INSTANCEZ_PUBLISHABLE_KEY)
We ran that file, with the new line, against inz dev. We seeded two clinics, a doctor at the first, a receptionist at the second and three patients. Signed in as the doctor, todaysAppointments() returned the first clinic’s two appointments with patient and clinician names joined in, and none from the other clinic. uploadReferral() stored the PDF under <clinic_id>/<patient_id>/referral.pdf. Signed in as a patient, myAppointments() returned one row, their own, and the same upload call got Not authorized to write this object.
instancez implements the Supabase HTTP API, so supabase-js doesn’t know it’s talking to something else. The compatibility page lists every supported call and the few that differ.
What replaces my migrations folder?
On Supabase, this app’s schema lives in SQL migration files (or declarative schema files) plus settings in the dashboard. On instancez, the whole backend is one instancez.yaml. Here’s the part the code above touches: the appointments table, the documents bucket and the my_clinic function. The full 160-line file adds clinics, staff, patients, a day_schedule RPC and a Node.js function.
tables:
appointments:
rls_enabled: true
fields:
- name: id
type: uuid
primary_key: true
default: uuid_v4()
- name: clinic_id
required: true
foreign_key: { references: clinics.id }
- name: patient_id
required: true
foreign_key: { references: patients.id }
- name: clinician_id
required: true
foreign_key: { references: staff.user_id }
- name: starts_at
type: timestamptz
required: true
- name: ends_at
type: timestamptz
required: true
check: "ends_at > starts_at AND ends_at <= starts_at + interval '4 hours'"
- name: status
type: text
required: true
default: scheduled
enum: [scheduled, checked_in, completed, cancelled, no_show]
indexes:
- columns: [clinic_id, starts_at]
rls:
- operations: [select, insert, update, delete]
using: "clinic_id = my_clinic()"
with_check: >-
clinic_id = my_clinic()
AND patient_id IN (SELECT id FROM patients WHERE clinic_id = my_clinic())
AND clinician_id IN (SELECT user_id FROM staff WHERE clinic_id = my_clinic())
- operations: [select]
using: "patient_id IN (SELECT id FROM patients WHERE user_id = auth.uid())"
storage:
documents:
max_size: 20MB
types: [application/pdf, image/jpeg, image/png]
rls:
- operations: [select, insert, update, delete]
using: "split_part(name, '/', 1) = my_clinic()::text"
with_check: "split_part(name, '/', 1) = my_clinic()::text"
rpc:
my_clinic:
language: sql
volatility: stable
security: definer
auth_required: true
set: { search_path: public }
returns: { type: uuid }
body: |
SELECT clinic_id FROM staff WHERE user_id = auth.uid();
Each rls entry becomes a real Postgres policy, and Postgres allows a row if any policy matches. Staff get their own clinic’s rows through my_clinic(). Patients get select on appointments linked to their own account. So the app code never filters by clinic or patient. Postgres does it. The documents bucket uses the same function on the first folder of each path, and a patient has no clinic, so their upload is refused. enum and check become Postgres constraints, so a booking with status: 'maybe' or an end before its start fails for every client.
You don’t write migrations. instancez diffs the YAML against the live database and applies the difference. Additive changes apply right away. Renames are declared with renamed_from:. Under inz serve, a change that would drop a table or column is refused unless you pass --allow-destructive, and the error names what would be lost. We cover the details in migrations without migration files.
How do I run it locally?
Install the CLI, scaffold a project and start the dev server:
curl -fsSL https://get.instancez.ai | sh
mkdir clinic && cd clinic
inz init
inz dev --embedded-pg
--embedded-pg starts a Postgres 16 that ships inside inz, so there’s nothing else to install and no Docker. The publishable and secret keys print at startup and land in .development.env. The API is on http://localhost:8080 and a dashboard is at /dashboard. Local Supabase without Docker goes deeper on this loop.
While inz dev runs, it watches the file. We added a column to appointments:
- name: reason
type: text
and saved. The log printed config reloaded successfully and the next query returned reason on every row. We didn’t write a migration file or restart anything.
The rest of the toolchain reads the same file without a database:
inz validate # structure and references
inz vet # open policies, disabled RLS, hardcoded secrets
inz gen types --out database.types.ts # typed createClient<Database>()
On the full clinic file, inz validate reports Schema valid and inz vet reports no findings, with 21 of 21 checks passed. inz gen types produces the same shape as supabase gen types typescript, so createClient<Database>() keeps working if you already use generated types.
How does the migration from Supabase work?
The migration runs from the instancez Cloud dashboard in four steps:
- Connect. Choose “Migrate from Supabase” and paste your database connection string and service role key.
- Check compatibility. You get a report with counts of tables, users, storage buckets and functions, plus anything that blocks the move or needs attention afterwards. Nothing is copied yet.
- Migrate. instancez creates the project, applies the schema and RLS, then copies users, rows and storage files. You can watch each stage, and a failed run resumes where it stopped.
- Switch the client. Change the URL and key, as in the diff above.
Here’s what the tool carries over:
| What | How it moves |
|---|---|
| Schema | Tables and columns, written into instancez.yaml |
| Row-level security | Table policies, in the same file |
| Users | Same user IDs and password hashes, so people keep their passwords |
| Data | Every row, including auto-increment sequences |
| Foreign keys | Every one instancez can recreate exactly, added after the rows are copied |
| Storage | Buckets, files and storage policies |
| SQL functions | RPC functions with regular arguments, including SECURITY DEFINER ones, and policies that call them |
A community clinic moved an 8 GB Supabase instance this way and was back online in under an hour.
What needs manual work?
The compatibility report flags each of these for your project before anything is copied:
| What | Why |
|---|---|
| Some foreign keys | Ones instancez would recreate differently are skipped, and the report names each one |
| Triggers | Not copied, including ones on auth.users that create profile rows. Recreate them after the move |
| Edge Functions | Supabase runs them on Deno and instancez runs JavaScript on Node.js, so you port them by hand |
| Some SQL functions | Functions with OUT, INOUT or VARIADIC arguments, or that use realtime, vault, cron, net, graphql or pgsodium |
| Auth settings | OAuth provider secrets, email templates and SMTP settings. The report lists what to re-enter |
Supabase features instancez doesn’t have yet also stay behind: the ones listed further down, plus the log explorer and point-in-time recovery.
Before you start
- Run the compatibility check first. It’s free and doesn’t copy anything.
- Keep your Supabase project running until you’ve tested the new backend.
- If your app takes writes around the clock, plan a short write freeze for the final run.
- Hosted instancez runs in AWS us-east-1. If you need another region, self-host the binary.
To run the check, create a project on instancez Cloud and choose “Migrate from Supabase”.
Why does one binary matter?
Self-hosted Supabase is a Docker Compose stack of 11 services, each with its own image, config and upgrade cadence. instancez serves the REST API, auth, storage, SQL functions, JavaScript functions and the dashboard from one process. You bring Postgres 14 or later.
Running it in production on your own server is two commands:
inz validate
inz serve --migrate
inz serve reads INSTANCEZ_DATABASE_URL, INSTANCEZ_PUBLISHABLE_KEY and INSTANCEZ_SECRET_KEY from the environment or .production.env. In Docker it’s the same binary in an image. From the Docker guide, with the tag pinned to a release:
docker run -d \
-p 8080:8080 \
-e INSTANCEZ_DATABASE_URL="postgres://postgres:password@host:5432/instancez" \
-e INSTANCEZ_PUBLISHABLE_KEY="inz_publishable_your-key" \
-e INSTANCEZ_SECRET_KEY="inz_secret_your-key" \
-v $(pwd)/instancez.yaml:/app/instancez.yaml \
ghcr.io/instancez/instancez:0.0.10 \
inz serve --migrate
The same binary runs on a VM, on Kubernetes and on AWS Lambda. Upgrading means replacing one binary or one image tag. Our posts on self-hosting on a VPS, Docker or Kubernetes and on AWS Lambda walk through each.
If you’d rather not run a server, push the same file to instancez Cloud:
inz cloud login
inz cloud deploy --new # first deploy: create, link, push
inz cloud deploy # later deploys: shows a diff, asks before applying
The hosted runtime is the same Apache-2.0 software. See pricing for plans.
Why does one YAML file matter?
Because you can read the whole backend at once. A schema change, a new policy or a new bucket shows up as a diff of one file in a pull request. Code review covers the backend the same way it covers the frontend.
It also matters when you build with a coding agent. An agent that edits one declarative file is easier to check than one writing SQL migrations and clicking through a dashboard. Run inz vet on its change and read the diff. We ship an agent skill (npx skills add instancez/instancez) and an MCP server for instancez Cloud. More on that in one YAML file for your whole backend.
The dashboard follows the same rule. Under inz dev, a schema edit made in /dashboard is written back to instancez.yaml as a diff you review before it applies. The file stays the source of truth.
What doesn’t come with you?
These Supabase features aren’t in instancez today:
- No Realtime or websockets.
supabase.channel()won’t work. - No phone or SMS login.
- OAuth covers Google, GitHub and Apple only.
- Image transforms output JPEG and PNG, not WebP or AVIF.
- Functions run on Node.js, so Deno Edge Functions need porting.
- No database branching yet.
Side by side:
| instancez | Supabase | |
|---|---|---|
| Client library | @supabase/supabase-js, unchanged | @supabase/supabase-js |
| Schema | One declarative YAML file | SQL migrations plus dashboard; declarative schemas available |
| Self-host footprint | One binary + Postgres | 11-service Docker stack |
| Local dev | inz dev --embedded-pg, no Docker | supabase start, the stack in Docker |
| Auth | Password, magic link, OTP, anonymous, TOTP MFA, Google/GitHub/Apple | All of that plus phone/SMS and more OAuth providers |
| Functions | JavaScript on Node.js | Edge Functions on Deno |
| Realtime | Not supported yet | Yes |
| License | Apache-2.0 | Apache-2.0 and MIT, per component |
The full breakdown, including hosted plans, is on /compare.
FAQ
Do I have to rewrite my supabase-js code?
No, for the features instancez supports. Change the URL and key. Calls to auth, from(), rpc(), storage and functions.invoke() stay the same.
Can I try it before moving any data?
Yes. Run inz dev --embedded-pg locally and point a copy of your app at it. On Cloud, the compatibility check in the migration flow copies nothing.
Is my data locked in?
The data is plain Postgres and the schema is plain YAML. Leaving means pg_dump.
Is it production ready? instancez is early. It’s fine for side projects, internal tools and evaluation. Read the docs and run your own tests before you put critical workloads on it.