Before you start: you may not need to move at all
If your v0 project is a marketing page, a landing page, a portfolio or a component library, it has no data to persist and this guide is a waste of your afternoon. Deploy it to Vercel from v0 and get on with your life.
The same is true if you are still iterating on the design. Exporting to GitHub and wiring a database freezes you into a normal development loop, and the normal loop is much slower than typing a sentence into v0 and watching the layout change. Do the design work in v0 until the design is done. Only then bring it down.
What this guide is for: you generated something that works, users are supposed to type into it, and you have realised there is nowhere for what they type to go. That is the wall. Everything below is how you get over it — a v0 export, a real Postgres database, and a running app that keeps its data between deploys.
Budget about an hour if you have written a Prisma or Drizzle schema before, an afternoon if you have not.
What v0 gives you, and what it does not
Be clear about the boundary, because most of the confusion comes from expecting the wrong thing.
You get: React and Next.js App Router code, Tailwind classes, shadcn/ui components, sensible file layout, forms that validate on the client, and — this is the part that is genuinely hard to beat — a UI that looks designed rather than assembled. Often it will scaffold a server action or an API route stub as well.
You do not get: a database, a schema, migrations, a connection string, authentication that survives a refresh, or any guarantee that the data your components render is real. That last one catches people out. A generated dashboard usually ships with a hardcoded array at the top of the file:
const invoices = [
{ id: "INV-001", customer: "Acme", amount: 4200, status: "paid" },
{ id: "INV-002", customer: "Globex", amount: 1800, status: "pending" },
];
It renders perfectly. It is a fixture. Your job is to replace that array with a query, and everything else in this post exists to make that one line possible.
Step 1 — Get the code into GitHub
v0 will push a project to a GitHub repository directly from the chat, which is the path you want because it keeps the whole tree rather than a single block. Download the ZIP and push it yourself if you prefer:
git init
git add -A
git commit -m "v0 export"
git branch -M main
git remote add origin [email protected]:you/your-app.git
git push -u origin main
If you only want one generated block dropped into a project you already have, v0 blocks expose a shadcn CLI command instead, which pulls the component and its dependencies into your existing tree without disturbing the rest of it.
Now get it running locally before you change anything:
npm install
npm run dev
If the export does not build clean, fix that first. Debugging a build failure and a database wiring problem at the same time is how a one-hour job becomes a day.
Step 2 — Get a database and a connection string
You need a Postgres instance that is reachable over the internet, has automated backups, and does not go to sleep. A managed database starts at $3/mo and gives you all three; provisioning hands you a connection string in the standard form:
postgres://USER:PASSWORD@HOST:5432/DBNAME
Two things worth doing at this point, because they are annoying later:
- Create a second database for local development. Running migrations against production from your laptop is a habit that eventually costs you a table.
- Check whether your provider requires TLS. If it does, append
?sslmode=requireto the URL. A connection that hangs with no error is almost always this.
Put it in .env.local and confirm .env* is in .gitignore before your next commit:
DATABASE_URL="postgres://user:pass@host:5432/appdb?sslmode=require"
Step 3 — Define a schema
Pick Prisma or Drizzle. Prisma has better ergonomics and a real migration story; Drizzle is thinner, closer to SQL, and has a smaller runtime. Either is fine. Do not use both.
Prisma. Install and initialise:
npm i prisma @prisma/client
npx prisma init --datasource-provider postgresql
Write prisma/schema.prisma to match the shape of the fixture data your components already expect:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Customer {
id String @id @default(cuid())
name String
email String @unique
invoices Invoice[]
createdAt DateTime @default(now())
}
model Invoice {
id String @id @default(cuid())
amount Int // store money in cents, never as a float
status String @default("pending")
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
customerId String
createdAt DateTime @default(now())
@@index([customerId])
@@index([status, createdAt])
}
Then push it to the database and generate the client:
npx prisma migrate dev --name init
npx prisma generate
migrate dev writes a versioned SQL file into prisma/migrations/, which is what you want in a repository. prisma db push skips the file and syncs the schema directly — fine while you are still deciding what the tables look like, wrong once anything real is stored.
Drizzle. The same model, in TypeScript:
// src/db/schema.ts
import { pgTable, text, integer, timestamp, index } from "drizzle-orm/pg-core";
import { createId } from "@paralleldrive/cuid2";
export const customers = pgTable("customers", {
id: text("id").primaryKey().$defaultFn(() => createId()),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const invoices = pgTable("invoices", {
id: text("id").primaryKey().$defaultFn(() => createId()),
amount: integer("amount").notNull(),
status: text("status").default("pending").notNull(),
customerId: text("customer_id")
.notNull()
.references(() => customers.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (t) => ({
byCustomer: index("invoices_customer_idx").on(t.customerId),
}));
Generate and apply with npx drizzle-kit generate then npx drizzle-kit migrate.
Step 4 — One connection, not one per request
This is the mistake that takes down small apps, and it is invisible in development. Next.js hot-reload re-evaluates modules on every change, so a client constructed at module scope gets constructed again and again until the database refuses new connections. Use the singleton:
// src/lib/db.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
Drizzle needs the same treatment around its pool:
// src/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
const globalForDb = globalThis as unknown as { pool: Pool | undefined };
const pool = globalForDb.pool ?? new Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
});
if (process.env.NODE_ENV !== "production") globalForDb.pool = pool;
export const db = drizzle(pool, { schema });
Keep max honest. A small Postgres instance has a connection limit in the low hundreds; if you run several containers each holding a pool of 20, you will hit it.
Step 5 — Server actions or API routes
Both work. The rule that decides it: if your own UI is the only caller, use a server action. If anything else needs to call it — a mobile app, a webhook, a script, another service — write an API route.
Reading data needs neither. A server component queries directly, and this is the line that replaces the fixture array:
// src/app/invoices/page.tsx
import { prisma } from "@/lib/db";
export default async function InvoicesPage() {
const invoices = await prisma.invoice.findMany({
include: { customer: true },
orderBy: { createdAt: "desc" },
take: 50,
});
return <InvoiceTable invoices={invoices} />;
}
Writing from a v0-generated form is a server action. Note the two things v0 will not have written for you — validation and revalidatePath:
// src/app/invoices/actions.ts
"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { prisma } from "@/lib/db";
const CreateInvoice = z.object({
customerId: z.string().min(1),
amount: z.coerce.number().int().positive(),
});
export async function createInvoice(formData: FormData) {
const parsed = CreateInvoice.safeParse({
customerId: formData.get("customerId"),
amount: formData.get("amount"),
});
if (!parsed.success) {
return { error: "Check the amount and customer." };
}
await prisma.invoice.create({ data: parsed.data });
revalidatePath("/invoices");
return { ok: true };
}
Two warnings. A server action is a public HTTP endpoint with a nice syntax — the client-side validation v0 generated does not protect it, so validate on the server as above and check the user is allowed to do what they asked. And if you skip revalidatePath, the write succeeds and the page keeps showing the old data, which reads as "my database is not saving" when it saved fine.
The API route version, when you need one:
// src/app/api/invoices/route.ts
import { NextResponse } from "next/server";
import { prisma } from "@/lib/db";
export async function GET() {
const invoices = await prisma.invoice.findMany({ take: 50 });
return NextResponse.json(invoices);
}
Step 6 — Environment variables
Anything named NEXT_PUBLIC_* is compiled into the browser bundle. Everything else stays on the server. Your DATABASE_URL must never carry that prefix, and the reason people do it by accident is that a client component threw an error about an undefined variable — the fix is to move the query to the server, not to expose the credential.
Environment variables are read at build time as well as runtime in Next.js, so set them on the platform before the first build rather than after. A build that ran without DATABASE_URL will fail at prisma generate or bake in the wrong value.
Step 7 — Deploy
Point the platform at the GitHub repository. Framework detection reads package.json and next.config.js and fills in the build and start commands; SSL and a working subdomain come with the deployment, and a custom domain is a DNS record. Set DATABASE_URL in the environment, deploy, then run your migration against production once:
npx prisma migrate deploy
migrate deploy, not migrate dev — the latter is interactive and will happily offer to reset the database.
Sizing: a Next.js build is memory-hungry even when the running app is not. The Starter pack at $3/mo (0.5 vCPU, 512MB) runs a small app fine but can run out of memory during the build; Standard at $7/mo (1 vCPU, 2GB) is the safe default, with Performance at $18/mo (2 vCPU, 4GB) if you are doing image work or heavy server-side rendering. One pack is one deployment slot, and the database is separate from $3/mo.
If the build does fail, the AI DevOps agent reads the build log and tells you which of the four usual causes it was — missing environment variable, a Prisma client that was never generated, a Node version mismatch, or an out-of-memory kill — rather than leaving you to read 800 lines of webpack output.
Where v0 is better and you should stay
This is not a "leave v0" post. v0 is the fastest way to get from a description to a designed interface that exists today, and nothing in this guide changes that. Keep using it for exactly that.
The workflow that actually works is two-directional: design and iterate in v0, export when the shape is settled, wire the data layer in your editor, and go back to v0 for the next screen. Generated components are ordinary React files — you can paste a new one into a repository that already has a schema and a working database, and it will render against real queries with a five-line change.
Where v0 stops being the right tool is the part described above: schema design, migrations, connection pooling, authorisation on writes. Those are not weaknesses in v0, they are outside what a UI generator is for.
What it costs
v0 publishes: Free at $0/month with "$5 of included monthly credits" and a "7 message/day limit"; Plus at $30/user/month with "$30 of included monthly credits per user" and "$2 of free daily credits on login per user"; Business at $100/user/month with the same "$30 of included monthly credits per user"; and Enterprise at custom pricing. Additional credits can be purchased beyond the included amount.
Vercel publishes Hobby (free), Pro at $20/user/month and Enterprise (custom). Pro is metered above its included amounts: "Starting at $0.128 per hour" of Active CPU, "Starting at $0.0106 per GB-hr" of provisioned memory, "Starting at $0.60 per 1M" invocations, "10M per month included; then starting at $2 per 1M" edge requests, and "1TB / month included; then starting at $0.15 per GB" of fast data transfer. Vercel does not publish managed Postgres pricing on its pricing page, so we are not going to quote a database figure for it — check it in your own dashboard before you plan around it.
Prices verified 13 August 2026 against v0.app/pricing (v0.dev/pricing redirects there) and vercel.com/pricing. Figures not published on those pages are omitted here rather than estimated.
Hostwares side, for the app described above: Standard pack $7/mo plus a managed PostgreSQL database from $3/mo, flat, with automated backups included. Whether that matters to you depends entirely on your traffic — a metered plan is cheaper when nobody is visiting, and a flat price is cheaper when they are.