Changelog
Flare ships as three packages that move together — @flaredev/core,
@flaredev/cli and create-flare-framework. They always share a version.
npx flare --version # what this app is onpnpm add -D @flaredev/cli@latest # upgrade itYour tables will gain columns when you upgrade. That is the fix, and it is worth saying first so it is not a surprise.
A resource with a cover image had no Cover column. Four field kinds — file,
long text, json and markdown — were born list: false, so the framework
decided they looked untidy in a table and left them out. That is a judgement
about taste, and taste belongs to whoever wrote the descriptor; what it
actually achieved was sending people to look for a bug in their own code,
because a field they had declared and filled in was nowhere on the screen.
Every field is a column now. Hide the ones you would rather not see:
meta: field.json({ list: false }), // kept, not shownblurb: field.text({ list: false }),Readers can also hide columns for themselves from the table’s column menu, which is per person and changes nothing for anyone else.
Nothing had to be built for this. FileCell has drawn a thumbnail for an
image and a filename for anything else since it was written, and formatValue
truncates JSON to one line with “this goes in a table cell” in the comment
above it. The renderers were ready; the default was hiding them.
What stays off by default is the part about correctness rather than looks:
long text and markdown are still not searchable (a LIKE over paragraphs is
not a search), and json and file are still not sortable.
Upgrading an app runs a migration. Both stacks gain a role_permission
table, and the Next.js stack gains Role to go with the one Cloudflare
already had. npx flare migrate writes and applies it.
Roles and permissions, from the dashboard. /dashboard/roles: create a
role, delete one, and tick what each may do per resource and action. Assign it
from Users, and the grant decides what that person reaches — in the admin and
over the API, which read the same policy so they cannot drift.
Where permissions live was the question worth getting right. Flare’s policies are files, and that is why they are reviewable in a pull request, travel with the repository, and are the same on every machine before anything deploys. A database is none of those. So the two compose: the policy file is the floor and the database only ever widens it. Nothing ticked in a running dashboard can take away access your code grants, and nothing can lock an app out. A tick that came from a file is shown locked, with the filename in its tooltip.
The corner worth knowing: a resource with no policy file is open to every
signed-in user, so grants on it do nothing. Inventing a policy out of the
grants would be worse than useless — an app would start refusing people the
moment somebody opened that screen. The row says it is open and gives the
command that restricts it. Roles already held by accounts appear whether or
not they were registered, because flare user:role someone auditor is a
complete way to make one.
Bulk add. Every resource list gets it beside New: a full-height sheet of rows and columns you can type into, tab across, Enter down, and paste a whole block into — tab-separated rows off a spreadsheet fill from the cell you are in. Cells are checked against the same validators the form and the API use, so a batch with a bad row is refused whole rather than written halfway, and rows are written through the same path the CSV import takes, so hooks, policies and ownership all apply. File columns are included, behind a one-line Attach control rather than a dropzone per row.
Every password box has the eye. It was on the sign-up and reset fields and missing from sign-in and “your password” — so the app offered to reveal your password in one place and not the next.
Devices said 0000:0000:0000:0000:0000:0000:0000:0000. That is the IPv6
loopback written in full: correct, unreadable, and identical on every row. It
says “this computer” now.
Every “choose one” is searchable. Enum fields in generated forms, and the role fields in the Users screen, are comboboxes you can type into — a six-option enum is a list you scan, a sixty-option one is a list you search, and the descriptor does not say which you have.
flare gen resource <Name> --add 'phone:phone?, notes:text?' appends
columns. --fields replaces the block, so adding one column used to mean
restating every field you had, which is how a field gets dropped by accident.
New descriptors carry the settings people reach for on day two — icon,
group, titleField, defaultSort, perPage, softDelete, rateLimit —
written commented out with aligned notes and a link. A key you can see is a key
you will use.
Smaller things. The error page tells a stale tab from a broken app: a tab
running old code after an update, or after a different app started on the same
port, is offered a reload, because React’s “Try again” re-renders the same
stale modules and can only fail. The command builder pins
the version its commands belong to and passes --yes, so a copied command
runs to completion instead of asking the questions the form already asked.
Four places still told you to run prisma directly — gen resource, rm resource, gen apikeys and doctor — and now say flare.
flare migrate on Next.js now develops — change your CI if it relied on the
old meaning. It used to run prisma migrate deploy, which applies what is
committed and never invents a migration — so after flare gen resource it said
“No pending migrations to apply” and the only way forward was a raw prisma
command. Locally it now runs prisma migrate dev and then prisma generate
(Prisma 7 removed generator runs from migrate dev, and a client without the
new model fails the very next command). --name skips Prisma’s prompt. In CI
and production, use flare migrate --deploy — that is the old behaviour,
unchanged: committed migrations only, nothing invented.
You never type a prisma command. flare db:seed seeds the development
admin (admin@example.com / Password, role admin; safe to run twice), and every
surface that teaches the flow — the boxes after create, the dashboard’s empty
state, the quickstart, installation — now says the same flare commands,
identically, on both stacks. The app’s db:migrate and db:seed scripts are
thin aliases of the same commands, so they cannot drift.
Users are managed from the dashboard. /dashboard/users on both stacks:
everyone who can sign in, their role, verified, two-factor, joined. New
user from a dialog; role changes — including a typed, brand-new role — and
ban/unban from a row menu. All of it goes through Better Auth’s admin API, the
same path as flare user:add and flare user:role, and your own row has no
menu, because demoting or banning yourself is a lockout. A role is a string
your policies give meaning; the page says so and points at flare gen policy.
The account menu is three entries now: Account settings, Manage roles
(admins), Log out.
The first sign-in no longer faces an empty page. The overview gets a Users
stat card and a Users row in “Your data”, and its empty state is the three
commands that make a first resource — the same three the terminal printed. The
account you seed is told to you in a “Sign in with” box after create.
Observability on Next.js shows what the app can see — users, records, signed-in sessions, changes this week, with a link to Vercel for edge traffic — instead of three dashes marked “not connected” and instructions mentioning wrangler on an app that deploys to Vercel. The Costs page is Cloudflare-only now: it prices Workers, D1 and R2, which mean nothing on Vercel and Neon.
The auth screens live in app/(auth)/. Seven folders — sign-in, sign-up,
forgot-password, reset-password, verify-email, two-factor, invite — were
scattered across app/. They are one route group now, so the folder exists
and no URL changes. Verified on both routers.
A command builder and two tutorials. The builder turns
your choices — stack, storage, theme, sign-in — into the one create command
to copy. The tutorials build a contact app end to end, including a rename that
keeps its data and a policy tried against a second account, on
Next.js and Cloudflare.
Every step was run against a scaffolded app before it was written down.
Smaller things. next dev no longer announces “We detected TypeScript…
reconfigured your tsconfig.json” on first start — the template ships what Next
16 mandates. The Cloudflare dev server is pinned to port 3000, where every box
and document already said it was. Sidebar group labels are capitals with a
divider between sections. The app’s display name is capitalised — danlite is
Danlite on the sidebar, the sign-in screen and in emails — while machine names
(the Workers script, the bucket prefix) stay lowercase. Dashboard counts cache
for a minute rather than five, so a fresh seed:resource shows on refresh.
flare user:add --role no longer refuses when the role table exists but is
empty — a fresh Cloudflare app — which was blocking the seed that makes the
first admin. base.prisma explains in comments why its generator block is
Prisma 7’s shape, for everyone who stops and wonders.
flare gen migration --from-schema could not apply a rename on Next.js.
It ran prisma migrate dev --skip-generate, and Prisma 7 removed that flag —
the CLI answers unknown or unexpected option and refuses the whole command,
so the rename migration was written to disk and never applied. The flag is
gone; v7 does not run generators from migrate dev anyway, which is all it
was there to prevent.
While fixing it: --from-schema described itself as (Cloudflare only). It is
not. On Next.js it is the flag that turns a rename in a descriptor into an
ALTER TABLE … RENAME COLUMN, and without it Prisma emits a drop and an add
and the data goes with them. The help and the reference both say so now.
Prisma 7.10.1, up from 7.10.0, across prisma, @prisma/client and
@prisma/adapter-pg — the newest stable 7.x.
Pinned exactly, and worth knowing why: prisma@latest is an 8.0 release
candidate, and Prisma’s update notifier prints npm i --save-dev prisma@latest inside the output of prisma migrate. Following the advice an
app’s own commands give lands it on a prerelease Flare does not generate for.
npx flare doctor now checks this — it fails an app that has drifted off
Prisma 7, fails one whose three Prisma packages disagree on a major (a client
the installed CLI did not generate breaks on the first query, not at build),
and warns on mixed 7.x patches.
The generated Prisma client is git-ignored. lib/generated/ was not in a
new app’s .gitignore, so prisma generate output — hundreds of files
rewritten on every install and every build — was committed, and went stale the
moment somebody changed the schema without regenerating. This is Prisma’s own
advice for the v7 generator, and it is only safe now that 0.12.1 made
postinstall regenerate the client.
A new Next.js app would not start. pnpm dev failed on
Can't resolve './generated/prisma/client' before serving a page, and so did
flare user:add, which imports lib/db.
Prisma 7 generates its client into lib/generated/prisma, which is git-ignored
and therefore absent from every fresh checkout — including the one create had
just written. prisma generate ran in build and nowhere else. New apps now
run it from postinstall, so the first dev works and so does a teammate’s
first clone. Existing apps: add it yourself, or run pnpm db:generate once.
pnpm db:seed makes an account to sign in with. A new app had tables and no
users, and the dashboard was a wall. The seed writes
admin@example.com / Password with the admin role, through the same
flare user:add that makes any other account. It is safe to run again — the new
--skip-existing flag makes a second run a no-op rather than an error, which is
what a seed has to survive when somebody resets their database.
The box printed after create now names the database steps as well, because the
app genuinely does not work without them: docker compose up -d, db:migrate,
db:seed, then dev.
UploadThing: one variable, and the SDK is installed for you.
UPLOADTHING_APP_ID is gone. A v7 token is base64 JSON — { apiKey, appId, regions } — so the app id was always inside the token Flare already had, and
asking for it separately asked for a value that could not disagree without
being wrong. When it did disagree the failure was quiet: uploads succeeded and
every read 404’d from a subdomain belonging to another app. The driver reads it
from the token now. Existing apps can delete the variable; passing appId
explicitly still works.
Choosing UploadThing at create also puts uploadthing in the app’s
dependencies rather than printing “Install its SDK” after the install has
already finished — a step people skip, whose error arrives at the first upload.
A new app’s first install could stop and ask. msgpackr-extract reached
pnpm’s undecided-build list, which is ERR_PNPM_IGNORED_BUILDS and a failed
install, not a warning. It is refused, like sharp and @prisma/engines: it is
an optional native accelerator that msgpackr falls back from on its own.
Smaller things. The script that builds the shipped lockfiles no longer runs
the scaffolded app’s own postinstall: resolution writes no node_modules, so
prisma generate ran against a Prisma that was never installed and took the
release build down with it.
One error envelope — change this if you read the API from your own code. (#29)
There were four error shapes. { error } from most guards, { error, field }
from a unique conflict, { error, issues } with a path in each issue from
validation, and { error, issues } with a param instead from a bad query
string — plus a plain-text refusal from the security layer. A client had to
handle whichever one the guard that refused it happened to use.
Every error is now the same:
{ "error": "Number is already taken.", "code": "unique_violation", "issues": [{ "path": "number", "message": "Number is already taken.", "code": "unique" }]}error is the sentence to show a person and its wording can change between
releases. code is for your code: nineteen values, a closed set, stable.
issues is present whenever the answer is about fields.
What to change. If you read field, read issues[0].path. If you read
issues[].param, read issues[].path. Nothing Flare generates needs changing —
the forms and the import screen moved with it — so this only affects code you
wrote against the API yourself. Full list of codes in
one error shape.
Two things fell out of it: a store with an ownership policy and nobody signed in
answered 403 and now answers 401, because there is no identity to refuse;
and the security layer answers a browser navigation with a sentence and an API
request with the envelope, rather than plain text to both.
flare seed:resource works on the Next.js stack.
(#24)
The one-liner that fills a table from its descriptor was Cloudflare-only, which stopped being a footnote when Next.js became the default: the quickest way to get something to build against was missing from the stack most people now start on.
npx flare seed:resource Ticket 5kCloudflare still writes SQL straight at D1. Postgres goes through the app’s
own Prisma client, because hand-writing the literal syntax for a text[], a
jsonb column and a Postgres enum is three chances to be subtly wrong about
escaping, and the client already knows all three. About 2,000 rows a second
against local Postgres, against ~14,000 on D1.
--remote has no meaning here — Postgres has one DATABASE_URL and it points
wherever you point it — and the command says so rather than implying a second
database.
A store now says whether it is confined to one person’s rows. (#23)
createResourceStore built without currentUser quietly stopped applying an
own policy and returned every row. That is right for a seed and a hole in a
server action, and both were written the same way — so the dangerous reading
was the one you got by forgetting an argument.
createResourceStore({ resource, rows, policy, currentUser }); // confinedcreateResourceStore({ resource, rows, policy, system: true }); // on purposecreateResourceStore({ resource, rows, policy }); // throwsPassing both is allowed and means what it says: a job that wants the user for
an audit hook and still needs every row. Nothing Flare generates changes —
every route and the dashboard already passed currentUser, which is why this
went unnoticed. The call sites it catches are hand-written ones, and the first
one it caught was Flare’s own CI script.
A renamed field keeps its data. (#56, #75)
fullName: field.string({ rename: { from: "name" } })npx flare sync-typesnpx flare gen migration rename_name --from-schema# create migrations/0004_rename_people_name_to_full_name.sql (people.name → full_name, data kept)Renaming a field used to be a drop and an add, which emptied the column. It applies once — the migration history decides whether there is anything to do, so the declaration is safe to leave in place and safe to delete.
And the bug underneath it, which was worse. drizzle-kit asks whether a
column was renamed or dropped, needs a terminal to ask in, and when it has none
it prints the question as an error and exits 0. Flare read only the exit
code, so that came back as “no table changes” — and because the unresolved
rename stays in the diff, every later gen resource produced no migration
either, silently. A project could stop generating migrations altogether and give
no sign of it. The run’s output is now read alongside its exit code, and a run
that wrote nothing while printing an error is a failure whatever it exited with.
belongsTo("User") is a relation like any other.
(#73)
npx flare gen resource Report --fields 'title:string, reporterId:belongsTo(User)?'“Who reported it” is in nearly every app and is usually the first relation
somebody reaches for. It failed, and the error told you to generate a User
resource first — which would have made a second, competing user table, so the
advice was a trap. You get the real foreign key, the index and a searchable
picker on both stacks.
onDelete defaults differently here: set null when the field is optional,
because somebody closing their account should not be held open by a
lost-property report from two years ago. A required one is restrict.
The picker needed an endpoint over the accounts table, which needed a decision
about who may see whom. /api/users is not a directory: an admin gets the
searchable list, everybody else gets their own account, signed out gets 401.
See a relation to the signed-in account.
Rate limits per resource and action. (#36)
rateLimit: { create: "10/min", "*": "120/min" }Checked in the generated route, straight after the policy, counted per
signed-in account (or per IP when there is nobody signed in). A refused call is
429 with retry-after and the rate_limited code. Worth setting on the
actions that cost something — a create that sends an email, an export that reads
the whole table — rather than on everything, which is what the edge limiter is
for.
Counted in a Durable Object on Cloudflare and in Redis on Next.js, not in
Cloudflare’s rate-limiting binding: its window may only be 10 or 60 seconds and
its limit is fixed in the Worker’s configuration, so "500/hour" would work on
one stack and not the other. A declared limit with nothing configured to count
it logs once and allows, and flare doctor reports it. See
rate limits.
flare create --signup=open|invite|closed.
(#16)
Sign-up was open with no way to change it. The default stays open — a new
resource is admin-only and the first account becomes the admin, so a stranger
who signs up reaches nothing — but invite and closed are now one flag, or
one line in lib/auth-config.ts afterwards.
Enforced where the account is created, not on each sign-up route: there are four ways to end up with a new account and gating them one at a time is how one gets missed. The first account is always allowed, whatever the mode, or an app could never be set up.
Invitations live in Better Auth’s existing verification table, so switching an
app to invite needs no migration. Links are single-use and only the hash is
stored, which is why the link is shown once to copy. closed needs a way in, so
there is one:
npx flare user:add ada@example.com --role adminSee who may sign up.
flare doctor.
(#54)
npx flare doctorEleven checks, for when something does not work and you do not know why: Node’s
version, the package manager, the app’s @flaredev/core against the CLI,
missing environment keys, bindings, whether the descriptors load, generated
files that are stale or hand-edited, resources with no policy, a declared rename
with no migration, a rate limit nothing is counting, and migrations on disk
against the ones the database has run.
It changes nothing, so it is safe on a broken app — the only time anybody runs
it. A check that cannot run says so rather than passing, and the exit code is
1 only for a failure, never a warning.
Enum options can have punctuation in them. (#74)
npx flare gen resource Patient --fields 'bloodGroup:enum(A+,A-,B+,B-,O+,O-,AB+,AB-)?'Blood groups had to be spelled aPos/aNeg and mapped back by hand everywhere
they were shown. An option may now use any printable character except a space, a
comma, round brackets, a backtick or $.
This turned up something already broken: a Prisma enum member has to be an
identifier, so A+ becomes A_2b with the real value in @map — and Prisma’s
client speaks member names, not column values. The seed writer translated; the
runtime store did not. So on the Next.js stack every hyphenated option was
already broken: in-progress has always been the member in_progress, which
the admin showed and the validators then rejected. The store now translates in
both directions.
flare deploy names the resources that have no policy.
(#17)
A resource with no policy file is readable and writable by any signed-in user.
gen resource writes an admin-only policy, so this only happens to a resource
generated before 0.11.0 or one whose policy was deleted — but nothing said so at
the moment it mattered. A warning, not a refusal: an open resource is a real
choice. --allow-open says it was meant.
Uploads: three more places to put files, and images shrunk before they are sent.
STORAGE_DRIVER takes uploadthing, cloudinary and disk alongside the
default R2 bucket — disk writes to a directory, the way Laravel’s local driver
does, which is what a VPS or a single container wants.
Images are now optimised in the browser before upload, by default: a 5.51 MB
photo went to 257 KB in a real app, twenty-two times smaller, with the original
never leaving the device. Set NEXT_PUBLIC_OPTIMISE_UPLOADS=false to send the
original bytes instead. It is read at build time, so changing it is a rebuild
rather than a restart.
New apps are scaffolded on vinext 1.4.3, and there is now a written policy for depending on it. (#18)
vinext reached 1.0 in October; apps were still being created against a beta from
before the stable line existed. Six packages move as one set — vinext,
@vinext/cloudflare, react, react-dom, react-server-dom-webpack and
@vitejs/plugin-rsc — because bumping one alone gives you an app that will not
install.
Depending on vinext says which version each release is tested against, how an upgrade reaches an app you have already built, and what happens if vinext stalls: the app keeps running, and the fallback is Next.js on Workers through OpenNext.
The first changes that came from somebody else.
Seven pull requests are in this release. All of this is other people’s work, and it is the first time that sentence has been true of Flare.
From @PrinceBadru:
- Sorting by
idworks —?sort=id,?sort=-id, anddefaultSort: { field: "id" }in a descriptor. Both query engines always supported it; the parser and the descriptor validator refused it, and the OpenAPI document did not list it. (#11) - The OpenAPI document describes every string format.
username,ip,uuid,timezone,locale,currencyandpostcodehad no pattern, format or example in the spec, so a generated client validated less than the server did. The patterns match the runtime validators exactly. (#12) - IPv6 validation rejects what it should.
:,:::,12345::1and oversized groups were accepted by anipfield. Same PR removed a duplicatedapikeysbranch in the CLI’s generator dispatch. (#12) seedUseris in the generated seed again. The resource seed template destructuredinsertMany,fakeandlogbut notseedUser, so seeding an account you could sign in as meant writinguserandaccountrows by hand — and getting theaccountIdmapping wrong gives a 401 that explains nothing. (#7)- A trailing slash on
BETTER_AUTH_URLno longer silently 403s thenext-shopexample. AnOriginheader never carries one, sohttps://example.com/— which is what copying from a browser bar gives you — matched nothing. The CLI template already handled this; the example did not. (#9) - The Next.js README says how to migrate a production database. Vercel
builds do not run migrations,
prisma migrate devcannot make a shadow database on serverless Postgres, and Vercel setsPOSTGRES_PRISMA_URLwhere Prisma readsDATABASE_URL. Three ways to a 500 on a first deploy, now written down where you are standing when you hit them. (#10) - The
next-shopexample is off the vulnerable Next.js line, which Vercel refuses to build. (#8)
From @BillWGong:
- Three broken links in a checked-in agent skill, pointing beside the file
rather than into
rules/. (#1)
If you want your name here, the issues are triaged and the roadmap items have write-ups. Nobody starts from a blank canvas.
If you are on 0.10.0, upgrade: npm install is broken there.
Not by anything Flare did. @better-auth/passkey@1.7.7 pins its peer
@better-auth/utils to exactly 0.4.2; when @better-auth/core@1.7.14
shipped it began pulling 0.5.x through better-call, and npm refuses to
resolve the result. Every new app failed its first install. The whole
@better-auth/* family moves to 1.7.14, where the peer range is widened to
^0.4.2 || ^0.5.0. Existing apps: change the @better-auth/* and
better-auth versions in your own package.json together — they peer-depend
on each other by exact version, so a mixed set resolves no better than the
broken one.
Next.js is the default stack.
pnpm create flare-framework myapp now gives you Next.js 16, Postgres and
Prisma on Vercel. --stack cloudflare gives you the other one, unchanged.
Nothing about an existing app changes — the stack is recorded in its
package.json and read from there.
A new resource is closed until you open it.
flare gen resource now writes an admin-only policy beside the descriptor,
and the first account to sign up becomes the admin. Before this, a resource
with no policy was readable and writable by anyone who signed up, and sign-up
was open — so the quickstart, followed exactly and deployed, produced an admin
panel open to the internet.
The two halves go together: admin-only resources with nobody holding the role
is an app that cannot be opened at all, and the usual fix for that is to open
everything. Widen a policy with
flare gen policy Invoice --roles admin,staff, and grant roles with
flare user:role you@example.com admin.
The Next.js stack comes with its services.
A new Next.js app has a docker-compose.yml holding Postgres, MinIO and Redis,
and a .env already pointing at them:
docker compose up -dnpx flare migratenpx flare devMinIO speaks S3 like R2 does and the Redis proxy speaks Upstash’s REST
protocol, so lib/storage.ts and lib/redis.ts are the same code whether you
are on your laptop or on the hosted versions. Without Docker, point the same
variables at Neon, R2 and Upstash —
the quickstart has both paths. The Cloudflare stack still
needs neither: Wrangler simulates D1, R2 and KV locally.
Fields that only apply sometimes.
A billing cycle is required on a recurring project and meaningless on a one-off one. Say so where the field is declared:
npx flare gen resource Project --fields 'kind:enum(oneOff,recurring), billingCycle:enum(monthly,annually) if kind == recurring'The form shows the input only while the condition holds, the validator requires
it only while the condition holds, and the store nulls it when the condition
stops holding — that last one is why this cannot live in the admin form. Hiding
an input stops the form sending a value; it does not remove the value already
in the row. != and in (a, b) work too, and the column is always nullable
because the field is genuinely absent the rest of the time. See
the field grammar.
Smaller things. Next.js apps no longer ship @better-auth/drizzle-adapter,
a Drizzle adapter they never imported and the proximate cause of the install
failure above. The template lockfiles are per stack — one file built from a
Cloudflare app was being copied into Next.js apps as well, where it pinned
packages the app did not have and missed every package it did. Descriptor
options render as TypeScript rather than JSON.
Money is stored in whole minor units. This one needs you to do something.
Three 7p items add up to 0.21000000000000002 in binary floating point, and a
column of doubles drifts the same way — which is how a total comes out a penny
short. A money field’s column now holds whole minor units: integer on D1,
BigInt on Postgres, so nobody finds a $21m ceiling in a price column.
Nothing above the database changes. The API, the forms, the CSV export, computed values and the typed client all still speak the units people write: you send 19.99, the column holds 1999, you read 19.99 back.
If you have no money field, there is nothing to do here.
If you do, and it was created on 0.9.x, the column holds 19.99 and the new
engine will read that as 19.99 minor units. Convert the data when you take
the new lib/resource/store.ts, not before and not after. Multiply by the
currency’s exponent — 100 for most, 1 for JPY and other currencies with no
minor unit, 1000 for KWD and the Gulf currencies:
-- Postgres: the column type changes tooALTER TABLE invoices ALTER COLUMN total TYPE bigint USING ROUND(total * 100);
-- D1/SQLite: loosely typed, so only the values need movingUPDATE invoices SET total = CAST(ROUND(total * 100) AS INTEGER);A money field declared as int is affected the same way unless its currency
has no minor unit.
Next.js 16.0.4 carried two critical RCEs, and Vercel refused to deploy it.
A fresh app on the Next.js stack failed on its very first deploy with
“Vulnerable version of Next.js detected”. The pin is 16.3.8, which clears
all 36 advisories that version was sitting inside — among them unauthenticated
remote code execution in the image optimizer and on Windows hosts. Existing
apps: change next in your own package.json, since the pin only decides what
a new app gets.
Seeding somebody you can sign in as.
seedUser({ email, password, name?, role?, emailVerified? }) is on the seed
context now, on both stacks. It writes the user and the credential account
together, which is the part that is easy to get wrong by hand: Better Auth
finds a password account by its accountId column, and for a credential
account that column holds the user’s own id — not the provider’s name, and not
the email. Put the email there and the row exists, the hash verifies, and
signing in still answers “User not found”.
A trailing slash on BETTER_AUTH_URL made every sign-in a silent 403.
Better Auth compares the request’s Origin against it, and an Origin never
carries a trailing slash — so https://example.com/, which is what you get
copying a domain out of a browser bar or the Vercel dashboard, matched nothing.
lib/auth.ts now trims it and says so on startup, naming the variable and what
to set it to, and warns separately for a value with no scheme.
prisma migrate dev can no longer offer to drop your data unanswered.
Smaller things. Sorting a Postgres list by a money field threw
Do not know how to serialize a BigInt — cursors are built from the rows as
the adapter gave them, and Prisma hands back a bigint. The dashboard sidebar
spent more height on group headings than on its links, enough to put a
nine-link nav into a scrollbar. The Vercel guide has a
first-deploy database checklist: an empty
Postgres answers 500 on sign-up, and POSTGRES_PRISMA_URL is not the variable
Prisma reads.
0.9.0 shipped a lockfile pnpm 12 refuses.
It pinned vite@8.3.2 and @vinext/types@1.0.1, both published that morning.
pnpm 12 enforces a 24-hour minimum release age, so every new app failed its
first command with ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION until those
versions aged out. The lockfiles are regenerated past the cutoff.
This repo pins pnpm 11, which has no such policy, so nothing here could have
caught it. scripts/verify-lockfile.mjs now does, and getting it right took
three attempts: the check needs pnpm 12 by name, a directory that did not
just write the lockfile, and a cold store. Each of those, missing, makes the
check pass on a file a user cannot install.
Policies can say “only your own records”.
flare gen policy Invoice --roles '*' --own userId --own-except admin confines
each person to the rows whose belongsTo(User) field holds their id. It
applies to reads, writes and deletes, and to the dashboard’s counts.
API keys, for clients with no browser. flare gen apikeys adds
@better-auth/api-key and a key-management screen, so a script or a service
can call your API without a session.
Soft delete. flare gen resource Ticket --soft-delete stamps deletedAt
instead of removing the row, and the dashboard grows a Trash view with restore.
Three more field kinds: markdown (long text with a preview), tags (a
free list of labels) and json (a value whose shape is not declared).
Fixes. The CSRF guard refused writes from the right site on Next.js —
request.url ignores the Host header, so it now also compares
X-Forwarded-Host. flare update deleted files a generator had written.
app/ was never tracked by flare diff. The Prisma model is Apikey, not
ApiKey, because Prisma lowercases only the first letter and Better Auth looks
up prisma.apikey. pnpm build never worked off Windows: an unquoted
--filter ./packages/** was glob-expanded by the shell.
The Export button never worked.
It asked the store for 500 rows a page; the page-size cap is 100 and applied to everything, so every export came back “Invalid query.” and the button showed an error toast — for every resource, on both stacks, since the feature shipped.
The cap is there to stop a request from outside asking for ten thousand rows
at once, and should never have applied to the server paging through its own
data on purpose. store.list now takes the limit its caller may use; the
export passes its own, and a request from outside is capped exactly as
before.
Twelve more field types.
Seven string formats — username, ip, uuid, timezone, locale,
currency, postcode — each with its own validation, input and display. A
time zone is checked against the runtime’s own list rather than a regex, and
the input offers that list; a currency code, a UUID and a postcode are
normalised on the way in.
Three number shorthands: money, percent and rating. The column stays an
ordinary float or int — the shorthand decides the input and the display. Money
gets its currency where the number is and refuses negatives unless the
descriptor allows them. A rating is a row of stars, bounded 0–5 unless you say
otherwise. A percent is left alone, because 150% is a real number.
Two aliases: website for url, alongside the existing phone for tel.
flare rm resource also works on the Next.js stack now. It removed the files
and then reached for drizzle-kit to write a drop migration, on an app that has
no drizzle-kit.
New pages: relationships, covering one-to-many, one-to-one and many-to-many with how each appears in a form, and file uploads, with an example per category.
Cursor paging failed on the Next.js stack for any date-sorted list — which is every list, by default.
A cursor travels through a URL, so a Date crosses it as a number. SQLite
stores dates as milliseconds and wants that number back; Postgres wants a
Date, and answered “Argument lt: Expected DateTime, provided Int”. The
second page of the dashboard’s default view could not load.
The cursor now carries whether its column holds a date — the store knows that from the descriptor, and neither adapter can tell on its own — and the Prisma adapter converts it back before querying.
This had a test, and the test asserted the broken behaviour: a fake delegate
accepts a number where Postgres will not, so { createdAt: { lt: 5 } } looked
right. Found by running a real query against a real database and reading the
SQL.
flare create also no longer stalls for seven seconds before printing
anything: it asked whether pnpm existed by running pnpm --version, and now
reads PATH.
flare diff was only looking at a sixth of the code Flare copies.
It tracked lib/resource and nothing else, so an app could report “up to
date” while its storage adapter, cache, API helpers and every dashboard
component were an old version. 0.7.1’s upload fix lives in lib/storage.ts —
a file flare update could not see, so the fix could never reach an app that
already existed. That is the one job these commands have.
They now cover everything under lib/ and components/ — 116 files in a
fresh app rather than 7 — minus the copies that are the app’s own: anything
rewritten from a placeholder, and lib/auth-config.ts, which is written from
the sign-in methods chosen at create time and differs from its template in
every app there will ever be.
Image uploads on the Next.js stack stored nothing, and said they worked.
Three faults in the same few lines of lib/storage.ts, found by uploading an
actual PNG rather than reading the code. The request body arrives as a stream
and S3 refuses a PUT it cannot measure — 411 MissingContentLength — so it is
buffered now, with the length set by hand, because Next patches the global
fetch and that one does not derive content-length even from a buffered body.
The adapter never checked the response, so a rejected upload returned 201 and
the field stored a key pointing at nothing. And the whole object key went
through encodeURIComponent, turning products/2026/09/a.png into a single
segment containing %2F.
Verified against a real S3 server: signed, uploaded, read back, byte-identical.
flare user:role also works on this stack now. It looked for a D1 database
and failed with “No d1_databases in wrangler.jsonc” — on an app that has no
wrangler.jsonc.
Routes say what they do.
0.6.0 moved the engine into your app, but a generated route still read as
eighteen lines of wiring — createResourceHandlers({ ... }), then four
exports. The code was yours and you still had to be told where it was.
A route now contains its own handlers. Each shows the policy check, the
cross-origin guard, the JSON read, the store call and the response it builds,
in the order they happen. Nothing is repeated between them: the guards are
named functions in lib/resource/http.ts, so the CSRF check, the
content-type check and the body drain that stops an early return breaking the
next request through wrangler’s dev proxy all still live in one place.
createResourceHandlers is still there and still works. Routes generated
before this release keep running; regenerate one with
flare gen resource <Name> --force to get the longer form, or leave them.
The Prisma engines download could fail, and took the install with it.
0.6.1 allowed @prisma/engines to run its postinstall, which downloads engine
binaries. When that download fails — a slow link, a proxy, anything —
pnpm install failed with it, after the packages were already on disk, so the
app looked installed and wasn’t.
It is refused now. A Flare app on the Next.js stack reaches Postgres through a
driver adapter and never needs what that postinstall fetches. Confirmed without
it: prisma generate, prisma validate, prisma migrate diff and
next build all work, and the install is quicker for it.
A new Next.js app couldn’t finish its first install.
pnpm install stopped with ERR_PNPM_IGNORED_BUILDS, naming four versions
of esbuild. 0.6.0 gave each stack its own list of dependency build scripts to
allow, and esbuild was left off the Next.js one — the Vercel CLI depends on
it, so every new Next.js app hit this on the very first command after
scaffolding.
The two lists are now one, the union of what either stack needs. Naming a package a stack doesn’t install costs nothing, because the entry is never consulted; leaving one out breaks the first thing somebody runs. Both stacks’ entries are pinned by tests, and the fix was confirmed by running both installs rather than re-reading the list.
The code that runs your endpoints moved into your app.
Until now a generated route was eighteen lines that called
createResourceHandlers, and the eight hundred lines that actually did the
work — query parsing, validation, hooks, pagination, constraint errors, the
database adapters — sat in node_modules. You could not read them, and you
certainly could not change them.
They now live in your repository, at lib/resource/, on both stacks. Nothing
generated imports the framework’s server entry any more. Ctrl-click
createResourceHandlers and you land in your own code.
Nothing is hidden explains the reasoning and states
both halves of the trade, because there is a cost: a fix in a later release
no longer reaches you on its own. flare diff shows what changed upstream
and flare update applies it, refusing to run without --yes.
The store takes rows now instead of a table and a database, so the adapter
is named where you can see it — drizzleRows(products, getDb) or
prismaRows(prisma.product, prisma). You can tell which database a route
talks to by reading the route.
Upgrading takes no work. An app made before the move has no
lib/resource/, and its next flare gen resource writes one, naming each
file as it goes. Routes generated by 0.5.x keep running untouched, because
the package still exports what it always did.
Also in this release: flare dev, build, start and deploy work on the
Next.js stack, delegating to Next and the Vercel CLI instead of telling you
to run something else. Every app gets a 404 page, an error page and loading
skeletons shaped like the page they wait for. File fields show a thumbnail
rather than an object key. Dependencies install with pnpm whenever it is on
the machine, because an app is around 340 packages and npm takes minutes
where pnpm takes seconds. And there is an
agent skill, an llms.txt and a prompt on every
docs page, for building with an AI.
The stack question, which 0.5.0 shipped without.
flare create --stack next worked, so every test passed and the gap stayed
invisible: the interactive flare create never asked which stack you wanted.
Anyone who answered the prompts rather than passing flags got Cloudflare and
was never offered the choice, which is the opposite of the point.
It is the first question now, because it decides the database, the ORM and
the host that everything else is built around. The docs learned to mention
--stack too — it had been documented on the stacks page and nowhere a new
person looks first.
A second stack: Next.js, Postgres and Prisma.
flare create --stack next builds an app on Next.js 16, Neon Postgres and
Prisma 7, from the same descriptors, with the same dashboard, the same
validators and the same policies. What changes is the schema, the migrations
and where it deploys. Choosing a stack compares them
properly, including what the Next.js one cannot do.
The resource store was split from its database access to make this possible: one interface, about fifty lines, with a Drizzle implementation and a Prisma one behind it. Everything a resource means stayed shared, which is why a unique-constraint error says “Sku is already taken” on both.
A catalogue on Next.js builds one against a real Postgres, with ranked full-text search and honest measurements — including the case where the index is the slower choice.
Selling a download, and the things that stopped it working.
The shop tutorial gained a till and digital delivery, the drive tutorial gained folders and multi-file upload, and building both turned up four gaps worth fixing rather than working around.
One deserves naming: every font 404’d in production. Tailwind inlines CSS
@import rules, so the fontsource imports never became real requests. Moving
them into the root layout emitted all fifty files.
flare deploy --yes arrived, and the file grammar was finally written down.
Your code, in plain sight.
Hooks, computed values and endpoints of your own, with a page explaining where each belongs and what regeneration will overwrite.
Tables grew up: a chart, chosen columns, saved views, editing in place, and pagination built for tables that got big. Every record got its own page, every account section got its own, and the sidebar got headings.
The dev server starts in about a minute now, rather than several.
One dashboard, and auth screens that say what they want.
The admin became a single coherent area rather than a set of generated pages, and every sign-in screen learned to explain itself — which method, what it needs, what happens next.
Seeding got serious: hundreds of thousands of rows, batched, with unique columns unique by construction rather than by retry.
Auth, themes, and an API that documents itself.
Every sign-in method — passwords, magic links, email codes, passkeys, two-factor by app or email, and social providers — chosen at create time and wired for you. Six themes, also chosen at create time.
Field types grew the formats that change both validation and rendering:
email, tel, url, domain, country, color, slug, plus select,
radio and multiselect.
Every app now serves an OpenAPI 3.1 document generated from its descriptors and its policies, rendered as a reference page, so the documentation cannot drift from the API.
The first one.
flare create, flare gen resource, and a resource descriptor that becomes
a table, a migration, validators, a REST API, a typed client and admin
screens.
The full history is in CHANGELOG.md and the GitHub releases.