Supabase → Kuunda Cloud migration

Auth, SDK, cutover. SQL export and console wizard: Import to Kuunda Cloud (schema.sql + data.sql).

1. Overview

SupabaseKuunda Cloud
Schema publicTenant schema proj_{uuid32hex}
https://{ref}.supabase.cohttps://{ref8}.kuunda-cloud.com
@supabase/supabase-js@kuunda/kuunda-js
Auth + Storage in the same DBSeparate Auth; helpers auth.uid() recreated in the tenant

Import wizard: Project → Settings → Data import (https://app.kuunda-cloud.com).

SQL editor (project schema)

The SQL editor and POST /migrate run in the project schema proj_…, not public — same as any Postgres dump whose source schema is public. The import wizard rewrites the schema; pasting an unadapted script into the editor often fails.

TopicKuunda rule
public. schemaDrop the public. prefix. Session search_path is already the project schema. Omit SET search_path = public.
auth.usersNo SELECT on the Auth catalog (separate database). Helpers: auth.uid(), auth.email(), auth.role(), auth.jwt(). Signup metadata: auth.jwt() -> 'user_metadata'. In the editor these functions return NULL (no user JWT); they evaluate through the authenticated REST / RPC API.
SQL commentsComments and string literals are not treated as executable SQL.
schema.sql dumpsPrefer Project → Settings → Import data: rewrites public. → proj_…, keeps auth.uid(), ignores auth.users FKs.

Editor-compatible example

CREATE OR REPLACE FUNCTION ensure_user_profile(
  p_email text DEFAULT NULL
)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
  v_uid uuid := auth.uid();
BEGIN
  IF v_uid IS NULL THEN RAISE EXCEPTION 'Not authenticated'; END IF;
  INSERT INTO profiles (id, email)
  VALUES (
    v_uid,
    COALESCE(NULLIF(TRIM(p_email), ''), COALESCE(auth.email(), ''))
  )
  ON CONFLICT (id) DO NOTHING;
END;
$$;
GRANT EXECUTE ON FUNCTION ensure_user_profile(text) TO authenticated;

2. Prerequisites

On your PC: a terminal (PowerShell or bash) and a PostgreSQL client with pg_dump.

On Supabase: database password (the Connect button, or Settings → Database).

On Kuunda: an account at https://app.kuunda-cloud.com/register.

ResourceWizard limit
SQL file~20 MB
Auth accounts2,000
Storage50 buckets, 2,000 files, 50 MB/file
Inventoried tables500

Above these limits: split the exports and repeat the imports.

3. Inventory before migration

ComponentMigration
Tables, views, functions, triggers, RLS✅ SQL export + wizard
Auth (email / password)✅ Import auth.users button
OAuth (Google, GitHub…)⚠️ Reconfigure providers
Storage✅ Import Storage button
Realtime✅ Automatic sync on import
Edge Functions❌ Manual redeploy
DB webhooks, pg_cron, Vault❌ Manual recreation
PostgreSQL extensions⚠️ Database → Extensions
SMTP / email templates❌ Auth → Emails

4. Create the Kuunda project

  1. Kuunda console → New project
  2. Settings → API — note:
    • URL: https://xxxxxxxx.kuunda-cloud.com
    • Key anon : kuunda_anon_…
    • Key service_role : kuunda_service_… (server only)
    • Tenant schema: proj_ + 32 hex characters

5. Configure Auth (before importing users)

On Supabase

Authentication → URL Configuration (Site URL, Redirect URLs) and Providers (OAuth Client ID / Secret).

On Kuunda

Auth → Providers: reuse the same URLs, enable the same providers, and register the new callback URI shown in Kuunda.

SaaS production: https://api.kuunda-cloud.com/auth/v1/callback. Update it at Google / Apple. Keep the Supabase URI until cutover is validated. Details: Google sign-in (OAuth).

6. Export the Supabase database

Commands, credentials (URL / URI / password) and where to paste them: Import page. Method: schema.sql then data.sql via pg_dump on the direct host db.{ref}.supabase.co (not the all-in-one dump, not the transaction pooler on port 6543).

7. Import into Kuunda

URL: https://app.kuunda-cloud.com/{ref}/settings/import

Step 1 — Source

  1. BaaS export (public schema) card
  2. Paste the Supabase Postgres URI (optional, for counts)
  3. Source schema: public
  4. Analyze source (counts)

Step 2 — Import accounts

After schema.sql, Source step → Import auth.users

KeptNot migrated
Same UUIDs, bcrypt passwords, metadataSupabase sessions / JWTs, OAuth identities, accounts without email

Users will need to sign in again. On the first OAuth sign-in, Kuunda links the account by email.

Step 3 — Import Storage

FieldValue
Storage URLhttps://{ref}.supabase.co
service_role keySettings → API → Legacy → service_role (eyJ…)

After data.sql, click Import Storage. Without URL + key: empty buckets only.

Step 4 — Apply SQL

Order: schema.sql auth.users data.sql Storage. Field details: Import page.

Before the schema: required extensions (e.g. pgcrypto, already CORE) via Database → Extensions.

  1. Script step: upload schema.sql
  2. Transform → proj_…
  3. Apply on Kuunda
  4. Back to Source → Import auth.users
  5. Repeat Script / Transform / Apply with data.sql
  6. Source → Import Storage
  7. Source ↔ Kuunda report — Source counts = Kuunda

Migrated: tables, RLS (auth.uid()), functions, triggers, views, Realtime sync. Ignored in SQL: FKs to auth.users / storage.*, Supabase roles, COMMENT ON SCHEMA, cluster-level event triggers (CREATE EVENT TRIGGER), blocked CREATE EXTENSION (install via the Extensions UI).

SQL import troubleshooting

MessageCause / action
transaction is aborted (often on the first DDL statement)Means a previous statement in the same batch failed. Check the dashboard version (savepoint fix for session_replication_role). On a partial import, drop the conflicting object then re-apply, e.g. DROP TYPE IF EXISTS proj_…mon_enum CASCADE; or DROP TABLE IF EXISTS proj_…ma_table CASCADE;
type … already existsPartial import — drop the type or table, or start from a fresh Kuunda project.
function gen_random_uuid() does not existInstall pgcrypto (Database → Extensions) before applying the schema. CORE already provides it on Kuunda.
permission denied for schema auth (CREATE POLICY)Fixed on the Kuunda side: the proj_…_owner role gets USAGE/EXECUTE on auth.uid() before import. Update the dashboard then re-apply the schema.
permission denied for schema publicRemove public. and SET search_path = public. The project schema is already the search_path. Or use the import wizard (automatic rewrite).
Forbidden reference … (auth.users)No access to auth.users. Use auth.uid(), auth.email(), auth.role(), auth.jwt(). A comment containing auth.users is no longer blocked.
File > ~20 MBSplit (--schema-only + --data-only) or remote migrations in chunks.

8. Realtime

Import adds tables to the Realtime publication. If tables are missing: Console → Realtime → Sync publication.

9. Edge Functions, cron, secrets

ItemAction
Edge FunctionsRetrieve the code → redeploy in Kuunda → Functions
SecretsCopy them manually
Webhooks, pg_cron, VaultRecreate in Kuunda

10. Adapt the application

Environment variables

Before (Supabase)After (Kuunda)
NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_KUUNDA_URL = https://{ref8}.kuunda-cloud.com
NEXT_PUBLIC_SUPABASE_ANON_KEYNEXT_PUBLIC_KUUNDA_ANON_KEY = kuunda_anon_…
SUPABASE_SERVICE_ROLE_KEYkuunda_service_… — server only

SDK

import { createClient } from '@kuunda/kuunda-js';

const kuunda = createClient(
  process.env.NEXT_PUBLIC_KUUNDA_URL!,
  process.env.NEXT_PUBLIC_KUUNDA_ANON_KEY!,
  {
    dbSchema: 'proj_uuid32hexsanstirets',
    projectRef: 'abcdef12',
  }
);

Key points

API compatibility

APIStatus
.from().select/insert/update/delete
.rpc()
.auth.signUp / signIn / signOut
.auth.signInWithOAuth✅ with @kuunda/kuunda-js
.storage.from()
Realtime
Edge Functions⚠️ Redeploy before use

SDK Auth differences

// getSession — synchronous
const session = kuunda.auth.getSession();

// getUser
const { data: user, error } = await kuunda.auth.getUser();

// onAuthStateChange — a single argument
kuunda.auth.onAuthStateChange(({ event, session }) => { … });

11. Production cutover

  1. Validate the DB import (count report OK)
  2. Test email + OAuth sign-in (same UUID for imported users)
  3. Test Storage and RLS
  4. Deploy the app with @kuunda/kuunda-js
  5. Add the Kuunda OAuth callback (keep Supabase for rollback)
  6. Maintenance: stop Supabase writes, delta import if needed
  7. Switch DNS / env vars to Kuunda

Delta import: re-export changed tables or remote migrations (POST /api/projects/{ref8}/migrate/service-role).

12. Final validation

TestExpected result
Table countsSource = Kuunda
Email/password sign-inSame password, active session
OAuth sign-inSame UUID as before
RLSanon blocked, authenticated OK
StorageUpload + download OK
RealtimeEvent received
RPC.rpc('ma_fonction') returns the data

13. Order of operations

  1. Create a Kuunda project and note the ref, schema, and API keys
  2. Configure Auth (URLs + OAuth providers)
  3. Install the required PostgreSQL extensions (pgcrypto, etc.)
  4. Export Supabase → schema.sql + data.sql
  5. Kuunda Import: analyze the source
  6. Transform + Apply schema.sql
  7. Import auth.users
  8. Transform + Apply data.sql
  9. Import Storage
  10. Source ↔ Kuunda report
  11. Sync Realtime if needed
  12. Redeploy Edge Functions + secrets
  13. Adapt the app → @kuunda/kuunda-js
  14. Full tests
  15. Production cutover

14. What does not migrate automatically

SupabaseRequired action
Edge FunctionsRedeploy on Kuunda
Database WebhooksRecreate (triggers + HTTP)
Vault / secretsRe-enter manually
Missing extensionsDatabase → Extensions
Cron jobsExternal job or VPS cron
Hard-coded Storage URLsUpdate code and database
SMTP / email templatesAuth → Emails

Open the console