Da Bubble a un codebase vero, senza perdere gli utenti
Riscriviamo l'app Bubble su Next.js, Supabase e TypeScript strict. I workflow diventano Server Action tipizzate, il modello dati passa su Postgres con row-level security, e gli utenti rinnovano la password il giorno del cutover.
Il problema
Bubble funziona, finché non basta più
Bubble funziona per un primo MVP. Il builder visuale rilascia un prodotto che gira, la logica no-code sopravvive al primo giro di feedback e il founder testa l'idea senza dover assumere ingegneri. Il problema inizia quando il prodotto inizia a funzionare davvero. Le performance non scalano, la business logic diventa illeggibile dentro l'editor visuale dei workflow, il vendor lock-in si stringe, e ogni ingegnere a cui il founder ne parla si tira indietro non appena sente "è fatto in Bubble". L'MVP che aveva dimostrato l'idea è la stessa cosa che oggi impedisce all'azienda di andare oltre.
Come lo affrontiamo
I sei passi di una migrazione Bubble
Ogni passo ha una consegna che il team può vedere. Il cutover avviene in un singolo giorno con un piano di rollback documentato, non un venerdì pomeriggio con una preghiera.
- 01
Audit dell'app Bubble
Mappiamo il modello dati (tabelle Bubble verso tabelle Postgres), i workflow (ognuno diventa una Server Action), il setup di auth, le integrazioni di terze parti e la base utenti viva. L'audit produce uno scope scritto e un numero prima che venga scritta una riga di codice.
- 02
Traduzione del modello dati su Postgres
Ogni tabella Bubble diventa una tabella Postgres con foreign key corrette, indici e policy di row-level security. Le relazioni implicite in Bubble diventano vincoli espliciti in SQL. Lo schema diventa qualcosa che un ingegnere può leggere.
- 03
Porting della business logic in Server Action tipizzate
Ogni workflow Bubble diventa una Server Action TypeScript con uno schema Zod per l'input e un valore di ritorno tipizzato. La logica resta la stessa; il runtime ora è codice sotto source control con test dove servono.
- 04
Ricostruzione della UI col design system
Le pagine Bubble vengono riprogettate (raramente vale la pena copiarle uno a uno) e sviluppate col design system. Le schermate nuove sono accessibili, mobile-friendly, con supporto dark mode, e coerenti in un modo che l'editor visuale di Bubble non riusciva a garantire.
- 05
Migrazione degli account utente
L'auth Bubble migra a Supabase Auth. Gli utenti esistenti ricevono un'email di reset password il giorno del cutover; gli ID utente originali vengono preservati così i riferimenti storici nei dati restano intatti. Gli utenti con social login mantengono i loro provider.
- 06
Cutover con piano di rollback
DNS flip un sabato, parallel-running per una finestra definita, una strada di rollback documentata finché entrambe le parti non firmano. Bubble continua a girare in read-only per trenta giorni come fallback prima della dismissione finale.
Cosa consegniamo
Modello dati Postgres
Schema tradotto da Bubble con foreign key, indici e policy di row-level security in migration sotto source control.
Migrazione degli account utente
Utenti Bubble importati su Supabase Auth, ID originali preservati, link di reset password inviato al cutover.
Sostituzione dell'auth
Supabase Auth con social provider, magic link, MFA e SSO dove il buyer ne ha bisogno.
Business logic come Server Action
Ogni workflow Bubble ricostruito come Server Action TypeScript tipizzata con validazione input e scrittura DB transazionale.
Dashboard di amministrazione
Il cockpit interno che l'editor Bubble dava gratis, ricostruito come area admin vera col design system.
Pipeline email
Email transazionali e di lifecycle spostate dal plugin di Bubble a Resend con template sotto source control.
Migrazione dei file storage
I file conservati da Bubble spostati su Cloudflare R2 con signed URL e prefissi per tenant.
Billing Stripe dove applicabile
Subscription, Customer Portal, prorating, webhook event router. Relazioni di billing esistenti migrate su Stripe.
Supporto multilingua
Hreflang e URL localizzate dal primo commit. Tre lingue di default dove Bubble di solito ne spediva una.
Baseline di performance
Core Web Vitals misurati contro la baseline Bubble, con l'app nuova che fa meglio su ogni metrica.
Documentazione del codebase nuovo
Runbook, panoramica di architettura, design system come pacchetto versionato. Il prossimo team riprende il lavoro senza visita guidata.
Checklist di dismissione Bubble
Piano passo per passo per cancellare l'abbonamento Bubble, esportare i dati storici, chiudere il vendor lock-in.
Traduzione di un workflow Bubble in una Server Action tipizzata
Un workflow Bubble del tipo "Quando un pulsante viene cliccato" con cinque step diventa una singola Server Action TypeScript con uno schema Zod, una transazione di database e una strada di errore esplicita. La forma dell'action è verificabile in source control; l'editor visuale che nascondeva la logica sparisce.
La parte difficile di una migrazione Bubble non è lo schema del database (le tabelle Bubble si mappano pulite su Postgres) o la UI (le schermate nuove di solito stanno meglio riprogettate che copiate). La parte difficile è la business logic che vive dentro l'editor di workflow visuale di Bubble. Ogni workflow è un albero di condizioni, scritture sul database, chiamate API ed effetti collaterali che il team sta modificando da mesi. La traduzione deve preservare ogni comportamento mentre trasforma l'albero in codice che un altro ingegnere può leggere.
Il pattern qui sotto è quello che rilasciamo: un workflow Bubble ("Quando un utente clicca Upgrade a Pro") riscritto come una singola Server Action tipizzata. Lo schema Zod valida l'input, la transazione racchiude le scritture, la strada di errore è esplicita, il tipo di ritorno è quello su cui chi chiama fa switch. L'editor visuale che nascondeva la logica viene sostituito da codice che vive in source control.
1. Il workflow Bubble originale
In Bubble, il flusso di upgrade assomiglia a una colonna verticale di passi nell'editor di workflow. Leggerlo richiede di aprire ogni passo, leggere le sue condizioni, guardare le sue azioni di database e tracciare quali campi ogni passo scrive. Qui sotto la stessa logica scritta in TypeScript.
2. La Server Action tradotta
// app/[lang]/(app)/account/actions.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { createServerClient } from '@/lib/supabase/server'
import { stripe } from '@/lib/stripe/server'
import { getServerSession } from '@/lib/auth/server'
const Input = z.object({
targetPlan: z.enum(['pro', 'team']),
})
export type UpgradeResult =
| { tag: 'success'; subscriptionId: string }
| { tag: 'already_on_target'; currentPlan: string }
| { tag: 'no_payment_method'; portalUrl: string }
| { tag: 'error'; message: string }
export async function upgradePlan(raw: unknown): Promise<UpgradeResult> {
const session = await getServerSession()
if (!session) return { tag: 'error', message: 'unauthorised' }
const parsed = Input.safeParse(raw)
if (!parsed.success) return { tag: 'error', message: 'invalid input' }
const supabase = await createServerClient()
// Step 1: legge la subscription corrente. In Bubble è "Get current user's
// subscription"; in TypeScript è una singola query tipizzata.
const { data: subscription, error: readError } = await supabase
.from('subscriptions')
.select('id, plan, stripe_subscription_id, status')
.eq('tenant_id', session.tenantId)
.single()
if (readError) return { tag: 'error', message: 'subscription not found' }
// Step 2: guard sul caso no-op. In Bubble era una condizione "Only when"
// sul trigger del workflow; qui è una precondizione prima di qualunque
// side effect.
if (subscription.plan === parsed.data.targetPlan) {
return { tag: 'already_on_target', currentPlan: subscription.plan }
}
// Step 3: verifica il metodo di pagamento. In Bubble era una chiamata
// separata di plugin seguita da un ramo condizionale; qui è una singola
// lookup Stripe.
const customer = await stripe.customers.retrieve(
subscription.stripe_subscription_id,
)
if ('deleted' in customer || !customer.invoice_settings?.default_payment_method) {
const portal = await stripe.billingPortal.sessions.create({
customer: subscription.stripe_subscription_id,
return_url: `${process.env.SITE_URL}/account`,
})
return { tag: 'no_payment_method', portalUrl: portal.url }
}
// Step 4: aggiorna la subscription Stripe. La proration e la generazione
// della fattura sono responsabilità di Stripe, non nostra.
const updated = await stripe.subscriptions.update(
subscription.stripe_subscription_id,
{
items: [{ price: PRICE_LOOKUP[parsed.data.targetPlan] }],
proration_behavior: 'create_prorations',
metadata: { tenant_id: session.tenantId, plan: parsed.data.targetPlan },
},
)
// Step 5: aggiorna la riga subscription locale. Il webhook scatta anche
// lui e conferma la stessa scrittura; la riga è idempotente.
const { error: writeError } = await supabase
.from('subscriptions')
.update({
plan: parsed.data.targetPlan,
status: updated.status,
updated_at: new Date().toISOString(),
})
.eq('id', subscription.id)
if (writeError) return { tag: 'error', message: 'database write failed' }
revalidatePath('/account')
return { tag: 'success', subscriptionId: updated.id }
}
const PRICE_LOOKUP: Record<'pro' | 'team', string> = {
pro: process.env.STRIPE_PRICE_PRO!,
team: process.env.STRIPE_PRICE_TEAM!,
}
3. Cosa cambia per il team
Fare la review di questa Server Action significa una code review di un file solo. Leggere il workflow Bubble originale richiedeva di cliccare attraverso cinque tab dell'editor visuale. Trovare un bug nella versione nuova è un errore di compilazione TypeScript o un test mancante; trovare un bug nella versione Bubble era sperare che qualcuno se ne accorgesse prima di un cliente.
La migrazione non è una riscrittura fine a sé stessa, è una traduzione da un runtime che nasconde la logica a un runtime in cui la logica è il codice sorgente. Il comportamento del prodotto resta identico; quello che diventa possibile è l'audit trail, la type safety, e tutto quel lavoro di ingegneria su cui il team può finalmente fare affidamento.
4. Cosa l'utente non nota
Fatta bene, la migrazione è invisibile per chi usa il prodotto. Le schermate caricano più veloci, l'email di reset password arriva una sola volta nel giorno del cutover, e il sign-in successivo è identico all'ultimo. Il lavoro di ingegneria è enorme; l'evento visibile all'utente è "l'app oggi sembra più reattiva". E quella è l'asticella che ci diamo.
Stack correlati
Domande frequenti
Migriamo o estendiamo Bubble?
Migrare quando il prodotto funziona e il costo di restare su Bubble (performance, hiring, vendor lock-in) pesa più del costo di riscriverlo. Estendere quando stai ancora dimostrando l'idea e la baseline Bubble basta. La risposta onesta richiede una call di scoping; ti diciamo subito dove cade la linea per il tuo caso.
Quanto dura una migrazione Bubble?
Quattro-otto settimane per una migrazione SaaS greenfield tipica. Il numero esatto dipende dalla complessità del modello dati Bubble, dal numero di workflow distinti, dalle integrazioni di terze parti e dalla dimensione della base utenti. La fase di scoping produce un numero su cui pianificare un budget.
Come si confronta con il costo dell'abbonamento Bubble?
La migrazione è un investimento una tantum; il costo continuativo dopo la migrazione è l'infrastruttura applicativa (Vercel, Supabase, Stripe), che scala linearmente con l'uso e resta prevedibile. La fattura mensile Bubble sparisce. Modelliamo il payback period in scoping, di solito sei-dodici mesi.
Gli utenti possono mantenere i loro account?
Sì. Importiamo gli utenti esistenti su Supabase Auth preservando i loro ID originali così i riferimenti storici restano intatti. Gli utenti email-e-password ricevono un link di reset password il giorno del cutover; quelli con social login mantengono i loro provider senza re-link.
E i plugin Bubble da cui dipendiamo?
Ogni plugin viene mappato a un equivalente TypeScript durante l'audit. La maggior parte dei plugin (Stripe, SendGrid, Twilio, AWS S3, Google Sheets) ha controparti dirette nel nostro stack standard. I casi limite vengono scopati esplicitamente così il costo è visibile in anticipo.
Come preserviamo l'integrità dei dati attraverso il cutover?
Parallel-run con Bubble ancora source-of-truth per una finestra documentata. L'app nuova legge da una copia sincronizzata dei dati; una volta che il team firma, il source-of-truth si sposta. L'app Bubble resta in read-only per trenta giorni come fallback prima della dismissione finale.
Di chi è il codice dopo la migrazione?
Tuo. Il contratto cede la proprietà intellettuale alla consegna. La sorgente vive nei tuoi repository, l'infrastruttura sui tuoi account, il design system come pacchetto versionato. Qualunque sviluppatore con esperienza TypeScript e Next.js può prendere in mano il codebase dopo la consegna.
Raccontaci la tua app Bubble
Una call di scoping, un numero concreto nella prima risposta, niente recite da agenzia. La maggior parte delle migrazioni Bubble esce in quattro-otto settimane.