Blog

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:

  1. Connect. Choose “Migrate from Supabase” and paste your database connection string and service role key.
  2. 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.
  3. 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.
  4. Switch the client. Change the URL and key, as in the diff above.

Here’s what the tool carries over:

WhatHow it moves
SchemaTables and columns, written into instancez.yaml
Row-level securityTable policies, in the same file
UsersSame user IDs and password hashes, so people keep their passwords
DataEvery row, including auto-increment sequences
Foreign keysEvery one instancez can recreate exactly, added after the rows are copied
StorageBuckets, files and storage policies
SQL functionsRPC 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:

WhatWhy
Some foreign keysOnes instancez would recreate differently are skipped, and the report names each one
TriggersNot copied, including ones on auth.users that create profile rows. Recreate them after the move
Edge FunctionsSupabase runs them on Deno and instancez runs JavaScript on Node.js, so you port them by hand
Some SQL functionsFunctions with OUT, INOUT or VARIADIC arguments, or that use realtime, vault, cron, net, graphql or pgsodium
Auth settingsOAuth 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:

instancezSupabase
Client library@supabase/supabase-js, unchanged@supabase/supabase-js
SchemaOne declarative YAML fileSQL migrations plus dashboard; declarative schemas available
Self-host footprintOne binary + Postgres11-service Docker stack
Local devinz dev --embedded-pg, no Dockersupabase start, the stack in Docker
AuthPassword, magic link, OTP, anonymous, TOTP MFA, Google/GitHub/AppleAll of that plus phone/SMS and more OAuth providers
FunctionsJavaScript on Node.jsEdge Functions on Deno
RealtimeNot supported yetYes
LicenseApache-2.0Apache-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.