Choosing a stack
Flare’s job is to turn a resource descriptor into a database table, a REST API, validators and a dashboard. Where that runs is a separate question, and there are two answers.
npx flare create myapp # Next.js on Vercel (default)npx flare create myapp --stack cloudflare # Cloudflare WorkersWhat each one is
Section titled “What each one is”| Cloudflare | Next.js (default) | |
|---|---|---|
| Framework | vinext (Next-compatible RSC) | Next.js 16, App Router |
| Runs on | Cloudflare Workers | Vercel |
| Database | D1 (SQLite) | Neon (serverless Postgres) |
| ORM | Drizzle | Prisma 7 |
| Cache | Workers KV | Upstash Redis |
| Files | R2, Cloudinary or UploadThing | R2, disk, Cloudinary or UploadThing |
| Auth | Better Auth | Better Auth |
| Resend | Resend + React Email | |
| Payments | Stripe | Stripe |
| Styling | Tailwind v4 + shadcn/ui | Tailwind v4 + shadcn/ui |
Auth, email, payments, styling and the dashboard are the same on both. What changes is the runtime, the database and the ORM.
Which to pick
Section titled “Which to pick”Cloudflare if cost and reach matter most. It is the cheaper of the two by a distance — $5/month covers a great deal, R2 charges nothing for egress, and the app runs in every Cloudflare location without you arranging it. The trade-off is SQLite: D1 is a real database but it is not Postgres, and the Workers runtime is not Node.
Next.js if you need Postgres or Node. Real Postgres with extensions, window functions and full-text search; any npm package that assumes Node; Vercel’s preview deployments; and a much larger pool of people who have worked in exactly this stack before. It costs more — Vercel, Neon and Upstash each have their own bill — and you give up R2’s free egress unless you keep files there.
If you are unsure, start on Next.js — it is the default for that reason. Costs has the real numbers, and your descriptors, hooks, policies and components move between the two.
What is shared, and what isn’t
Section titled “What is shared, and what isn’t”Shared, because it is ordinary TypeScript with no runtime in it:
- Resource descriptors, hooks, computed fields
- Policies and roles
- Every React component in the dashboard
- Seeds, and the field type grammar
Generated per stack:
- The schema and migrations — Drizzle/SQLite or Prisma/Postgres
- The route handlers — Workers or Next.js Route Handlers
- The database client and the cache adapter
The same resource, on both
Section titled “The same resource, on both”This is the whole difference, in the files one flare gen resource Product
writes. Everything else in the app is the same code.
The descriptor — identical
Section titled “The descriptor — identical”export default defineResource({ name: "Product", fields: { name: field.string(), sku: field.string({ unique: true }), price: field.float(), category: field.belongsTo("Category", { required: false }), },});The schema — different language, same shape
Section titled “The schema — different language, same shape”export const products = sqliteTable("products", { id: text("id").primaryKey(), name: text("name").notNull(), sku: text("sku").notNull().unique(), price: real("price").notNull(), categoryId: text("category_id").references(() => categories.id, { onDelete: "set null" }), createdAt: integer("created_at").notNull(), updatedAt: integer("updated_at").notNull(),});model Product { id String @id @default(uuid()) name String sku String @unique price Float categoryId String? @map("category_id") category Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at")
@@map("products")}The route — one line apart
Section titled “The route — one line apart”import { createResourceHandlers, drizzleRows } from "@/lib/resource";// … rows: drizzleRows(products, getDb), // Cloudflare rows: prismaRows(prisma.product, prisma), // Next.jsBoth call the same createResourceHandlers — which is code in your app,
not in a package. The row adapter is the seam, and it is about 130 lines on
either side.
The commands
Section titled “The commands”| Cloudflare | Next.js | |
|---|---|---|
| Write a migration | flare gen migration --from-schema |
flare gen migration (wraps prisma migrate dev) |
| Apply it | flare migrate |
flare migrate (= prisma migrate dev + generate; --deploy for CI) |
| Dev server | flare dev |
flare dev (= next dev) |
| Deploy | flare deploy |
flare deploy (= vercel deploy --prod) |
| Fill a table | flare seed:resource Product 25k |
flare seed:make + flare seed |
Stack differences, in one table
Section titled “Stack differences, in one table”Everything that behaves differently, in one place. If a command or a feature is not here, it works the same on both.
| Cloudflare | Next.js | |
|---|---|---|
flare migrate |
applies pending D1 migrations | prisma migrate dev + generate (--deploy: committed only) |
| Writing a migration | flare gen migration --from-schema |
flare gen migration (wraps prisma migrate dev) |
flare seed |
the local D1 database | the database in DATABASE_URL |
flare seed:resource |
fills a table from its descriptor | not available — use flare seed:make <name> --resource <R>, then flare seed |
--remote, --env, --database |
target the deployed D1 database | ignored — there is one database, the one in DATABASE_URL |
flare dev / build / start |
vinext and wrangler | next dev / next build / next start |
flare deploy |
migrates, builds, pushes to Workers, syncs secrets and zone rules | vercel deploy --prod |
realtimeChannel().publish() |
delivered by a Durable Object per channel | does nothing, and warns once per channel saying so |
| Websocket connections | /realtime/<channel>/ws |
refused — authorizeRealtime returns false |
| Observability page | reads Cloudflare’s Analytics API | reports “unconfigured” and points at Vercel’s dashboard |
A json field’s key order |
kept as written (stored as text) | normalised (jsonb is parsed, not text) |
| Full-text search | LIKE over indexed columns |
the same, plus Postgres tsvector if you write it |
| Request CPU | 10ms free, 30s paid | the function’s configured timeout |
| Node APIs | Workers runtime — no filesystem, no native modules | all of them |
flare --help reads the stack from your package.json, so it describes the one
you are actually on rather than listing both.
Where they genuinely differ
Section titled “Where they genuinely differ”Beyond the table at the top, these are the differences that change what you can build.
The runtime. Workers is not Node. Most npm packages work; anything using Node’s filesystem, native modules or long-lived TCP does not. Next.js on Vercel runs Node, so everything works.
CPU per request. A Worker on the free plan gets 10ms of CPU per invocation, and 30 seconds on the paid one — plenty for a query and a render, not enough to resize video. Vercel functions run to their configured timeout.
SQLite versus Postgres. D1 is real SQL and fine for most applications.
Postgres gives you extensions, window functions, tsvector full-text search
with ranking, jsonb operators and materialised views. If you know you need
one of those, the choice is made.
Cold starts. Workers have effectively none. Neon’s free tier suspends an idle database, so the first request after a quiet spell waits for it to wake — a plan choice, not a code problem.
Egress. R2 charges nothing for bandwidth out. For an app that serves files, this is often the single biggest line on the bill, which is why both stacks use R2 for storage.
The framework underneath. The Cloudflare stack runs on vinext, which is younger than Next.js and is the dependency people hesitate over. Which version each Flare release is tested against, how an upgrade reaches an app you have already built, and what happens if vinext stalls are written down in depending on vinext — worth reading before choosing this stack for a client.
Realtime. Cloudflare gives every channel a Durable Object — one address,
its own storage, websockets that stay open. Vercel has no equivalent, so
realtimeChannel().publish() delivers nothing. It logs a warning the first
time, once per channel, rather than failing quietly; the write that triggered
it still succeeds.
Can I switch later?
Section titled “Can I switch later?”Not with a command, and there isn’t one planned. What moves without change:
your descriptors, hooks, computed fields, policies, seeds, every dashboard
component, and lib/resource/ itself apart from its row adapter. What
doesn’t: the schema and its migration history, the database client, the cache
adapter and the deployment.
In practice, moving a small app is an afternoon — regenerate the resources on a new app of the other stack and move the data. Moving a large one with a year of migrations is not.
The honest advice is to pick for the database. Everything else is either shared already or a day’s work; a schema with real data in it is neither.
Starting a Next.js app
Section titled “Starting a Next.js app”npx flare create shop --stack nextcd shopcp .env.example .env # DATABASE_URL from Neon, BETTER_AUTH_SECRETnpx flare gen resource Product --fields 'name:string, sku:string!, price:float'npx flare migrate # prisma migrate dev, then prisma generatenpx flare devflare migrate chains prisma generate, and that is not belt-and-braces:
Prisma 7 removed generator runs from migrate dev, so a bare prisma migrate dev adds the table and leaves the client without the model — and the next
command fails on “No Prisma model”. For CI and production,
flare migrate --deploy applies committed migrations and invents nothing.
flare gen resource writes the same route handlers, typed client, validators
and dashboard pages it writes on Cloudflare. What changes is the schema:
prisma/schema/resources.prisma instead of a Drizzle module per table.
prisma/schema is a folder of two files, and the split matters:
base.prisma— the generator, the datasource, and the tables Better Auth and the dashboard need. Yours to edit; never regenerated.resources.prisma— your resources, written from the descriptors. Overwritten every time you runflare gen resource.
flare gen migration <name> writes one. It wraps prisma migrate dev --create-only, so the migration is written and not applied, and then reads
the SQL back:
- If it would drop anything, it says which statement and what goes, and stops. Two things produce a drop and only you can tell them apart: a field you removed from a descriptor, or a column Prisma cannot see and therefore thinks should not exist.
- If Prisma reports drift, it stops before Prisma’s own suggestion — which is
prisma migrate reset, and that drops the database. Drift usually means something was added by hand, so resetting is exactly how you lose it.
An index, a trigger or a function is not reported as drift at all; Prisma does not look at those. They survive a migration and are lost by a reset, which is the other reason not to reach for one.
Underneath it is still Prisma’s: prisma migrate dev diffs the schema against
your database and writes the SQL. flare migrate applies what is already
written — it runs prisma migrate deploy here — which is the one you want
against a database that matters, because it never invents a migration.
flare dev, build, start and deploy work here too, delegating to Next
and the Vercel CLI instead of vinext and wrangler — so the commands are the
same on both stacks even though what they run isn’t. flare build runs
prisma generate first, and flare deploy is vercel deploy --prod with
your arguments forwarded. Deploying to Vercel
is the detail.
What this stack is missing
Section titled “What this stack is missing”Being straight about it:
- Realtime. Cloudflare gives every channel a Durable Object — one
address, its own storage, websockets that stay open. Vercel has no
equivalent, so
realtimeChannel().publish()delivers nothing — it warns once per channel and lets the write succeed.lib/realtime.tsis two functions in your app; a hosted pub/sub (Ably, Pusher, Upstash) behind them is the way to add it. - Traffic analytics in-app. The observability page reads Cloudflare’s analytics API on the other stack; here it points you at Vercel’s dashboard rather than holding a token that can read your whole account.