Same auth (Supabase) — users stay
Same UI (shadcn/ui + Tailwind)
Next.js App Router — same framework
Zero forced sign-outs

Why migrate from Base44?

Base44 gives you a working app without the setup pain. Already gives you everything a paying-customer app actually needs — hardened auth (TOTP, passkeys), idempotent billing webhooks, multi-tenant isolation, rate limiting, and a developer experience that scales past one person.

The stack is the same. The difference is 16 production modules that are already wired together rather than 16 separate things you'd eventually build yourself.

What carries over

  • Your users. Base44 uses Supabase Auth. Point Already at the same Supabase project URL — your users, sessions, and permissions are untouched.
  • Your UI. Both use shadcn/ui and Tailwind. Your components paste directly into Already's components/ directory.
  • Your data. Your Supabase tables stay in place. Add Drizzle schema declarations and Already can query them immediately.
  • Your API routes. Base44-generated API routes under app/api/ copy directly into Already's structure — the App Router conventions are identical.

Env var mapping

Base44 may use either a Vite or Next.js prefix depending on how the project was scaffolded. Check your Base44 project's env var names and rename accordingly:

# If your Base44 project used Vite prefixes:
VITE_SUPABASE_URL            → NEXT_PUBLIC_SUPABASE_URL
VITE_SUPABASE_ANON_KEY       → NEXT_PUBLIC_SUPABASE_ANON_KEY

# If your Base44 project already used Next.js prefixes:
NEXT_PUBLIC_SUPABASE_URL     → no change needed
NEXT_PUBLIC_SUPABASE_ANON_KEY → no change needed

What does NOT carry over as-is

Base44 generates a client/ and server/ separation that roughly maps to Already's Client Component vs Server Component split — but with one critical difference.

Base44's server-side queries may bypass RLS. If your Base44 app queries Supabase using the service role key or without the user's JWT, those queries have no tenant isolation. In Already, withOrgScope() wraps every query with the org context, enforcing row-level isolation automatically. Any Base44 query that touches tenant data needs to be wrapped.

// Base44 pattern — direct query, no tenant scope
const { data } = await supabase.from('projects').select('*')

// Already pattern — org-scoped query via withOrgScope()
import { withOrgScope } from '@/lib/org'
const rows = await withOrgScope(db.select().from(projects))

React Query or SWR data fetching in Base44 client components can stay if you prefer client-side fetching, but Already's preferred pattern is Server Components — data arrives at render time, no loading states needed for initial data.

Step-by-step

01
Connect to your existing Supabase project

Copy your Supabase project URL and anon key from the Supabase dashboard into Already's .env.local. Use NEXT_PUBLIC_ prefix regardless of what Base44 used. Run pnpm setup — it validates the connection and runs any new migrations without touching your existing data.

02
Copy your UI components

Base44 generates shadcn/ui components with Tailwind classes. Copy your components/ directory into Already's. Your design tokens carry over without changes.

03
Copy your API routes

Base44-generated app/api/ routes use standard Next.js App Router conventions. Copy them directly into Already's app/api/. Review each handler for unscoped Supabase queries and wrap them with withOrgScope() where they access tenant data.

04
Remap routes to Already's group structure

Move authenticated pages under app/(app)/, auth pages under app/(auth)/, and public pages under app/(public)/. Already's layout guards automatically protect each group — no per-page auth checks needed.

05
Add Drizzle schema for your tables

Declare your existing Supabase tables as Drizzle schema files in db/schema/. No data migration — you're just adding type-safe declarations for tables that already exist. If your column names differ from Drizzle's camelCase default, use the column option to map them explicitly (see FAQ below).

06
Wire billing (if not already in Base44)

Run pnpm setup:stripe to create Stripe products. Wrap paid routes with await requirePlan('pro'). The webhook handler, customer portal, and trial logic are pre-built.

07
already migrate base44

The included CLI command handles Vite/Next.js flavour detection, env var mapping, and generates a structured TODO report for what needs manual attention.

already migrate base44 --source ../my-base44-app

Frequently asked

Will my users get logged out?

No. Already connects to your existing Supabase project. Sessions and tokens are unchanged. Your users won't notice.

Base44 generated my schema with non-standard column names — what changes?

Nothing changes in Supabase — your tables stay exactly as they are. You add Drizzle schema declarations that match your existing column names. If your column is named created_by_user but Drizzle would default to createdByUser, use the explicit column mapping:

export const projects = pgTable('projects', {
  id: uuid('id').primaryKey(),
  createdByUser: uuid('created_by_user'),  // maps Drizzle field → actual column
})

This is purely a TypeScript declaration — no ALTER TABLE, no data movement.

My Base44 app uses React Query — do I have to remove it?

No. React Query works fine alongside Already's Server Components. That said, for initial page data Already's pattern is to fetch in Server Components so there's no client-side loading state. React Query is most useful for mutations and optimistic updates — those patterns carry over unchanged.

What if Base44 generated non-standard patterns?

The already migrate base44 CLI detects Base44-specific patterns and flags them. The migration guide inside the repo covers the common cases. For anything unusual, email [email protected].