What's new in Nextly
Latest releases, features, improvements, and bug fixes.
Released 19 packages at 0.0.2-alpha.58 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #720 `8a7e734` Thanks @mobeenabdullah! - Take the reference palette's light-mode values for the admin, and record what
- that costs where a reader will find it.
--nx-input, --nx-border-strong and --nx-sidebar-border move to the
reference border weight; --nx-destructive and --nx-destructive-solid move to
the reference red; --nx-sidebar-foreground matches the active nav ink so the
sidebar reads at body-text weight.
Several of those render below their WCAG minimum, deliberately. Each affected
pairing is listed in the new contrast/accepted.ts with the ratio it actually
measures, and the contrast suites hold every entry to three properties: it still
measures what is recorded, it is still below its threshold, and it still names a
token the theme declares. The sharpest is white on the destructive fill at
3.84:1, which is the label of the Delete, Discard and Unpublish confirm buttons.
Because resting and active sidebar ink are now one value in light mode, the active row also carries a font-weight change. A fill at 1.11:1 cannot identify a state on its own, and a weight difference is not a colour, so it is not subject to a contrast ratio at all.
Dark mode is unchanged apart from --nx-success, which moves a step lighter to
clear its minimum on the muted surface with the margin the suite requires.
Checkbox and radio take a new --nx-control-border rather than following the
field border down. A field is identifiable without its edge; an unchecked box is
only the box, so its boundary is held to 3:1 with no acceptance.
- #830 `f53dbd8` Thanks @mobeenabdullah! - Give every admin list one owner for its page state, and one source for its page-size policy.
Twelve list surfaces each held the same two useState calls plus their own handlePageSizeChange wrapper that set the size and then reset the page. They now use the existing usePagination hook, which owns those resets. That removes the drift risk across the copies and, more usefully, removes the wrapper entirely: onPageSizeChange is the hook's setPageSize, which resets the page in the same update, so a query keyed on both refetches once rather than twice.
The page-size options were a literal [10, 25, 50] written out at nine call sites. They are now PAGINATION.TABLE_PAGE_SIZE_OPTIONS, beside the existing page-size constants, so the policy can change in one place. The two lists that deliberately differ keep their own: the media grid offers 12/24/48/96 because it lays out thumbnails, and the delivery log offers 20/50/100 because it is read in long scans. usePagination's own defaults now derive from those constants rather than restating 0 and 10.
Pagination's pageSizeOptions is typed readonly number[], since the component only maps over it and a mutable type would reject the shared options for no reason a caller could act on.
Two lists stay off the hook and say why where they declare their state. The entries list is 1-indexed because that is what its API takes, and converting at every read and write trades one clear boundary for a class of off-by-one. The relationship picker accumulates results rather than paginating them: its page number only increments, results append, and there is no way back to a previous page.
- #840 `f5a5405` Thanks @mobeenabdullah! - Add
GET /api/admin-meta/workspace, a session-gated route serving the admin metadata that describes the installation: mounted plugins and their contributions, configured locales, custom sidebar groups, and builder availability./api/admin-metastill serves these alongside branding until the admin reads them from the new route, so nothing is withheld from an anonymous caller yet.
- #783 `376a3a4` Thanks @mobeenabdullah! - Repair the blog template so a scaffolded project type-checks and builds.
A new blog project failed its build at the search-index step: it has no posts, so Pagefind indexed nothing and exited non-zero. The empty case is now reported and skipped, while a real Pagefind failure still stops the build.
SQL statement splitting no longer tracks string state through comment text. An apostrophe in a retained comment opened a string that never closed, which merged every following statement into one that SQLite rejects.
The query layer narrows documents with runtime-checked readers instead of asserting them to its domain types, and a collection can declare defaultColumns in code as the admin and the visual schema already allowed. That option is now also carried through collection sync, which previously rebuilt the persisted admin shape in two places and dropped it in both.
Type change worth reading before upgrading: FindUsersArgs no longer inherits
the FindArgs options that users.find() does not implement — where,
status, sort, select, populate and pagination. Passing them compiled
and did nothing, so a where clause intended as an exact lookup returned the
first arbitrary user; code that passes one will now fail to compile. Use
search, or read a page and compare the field directly.
- #785 `8dc013e` Thanks @mobeenabdullah! - Refuse to start when boot migrations did not run. With
db.runMigrationsOnBoot, - an instance that could not take the migrate lock before its wait deadline used
- to log
Boot migrations complete (0 applied)and serve traffic —appliedis 0 - there and 0 on an up-to-date database, so nothing distinguished them. On a
- rolling deploy that is the second replica serving against a schema it never
- migrated. It now fails startup, which an orchestrator retries once the other
- instance finishes; a genuinely stale lock is cleared with
-
nextly migrate --force-unlock.
withMigrateLock reports whether its body ran instead of returning undefined
for both "returned nothing" and "never ran", so every caller has to decide. Its
wait-timeout message said "proceeding without it" while returning without
running the migrations, and now says they were skipped.
- #768 `7ea3567` Thanks @mobeenabdullah! - fix(create-nextly-app): generate a build script that runs on Windows, and stop swallowing a failed search-index build
- #752 `59d84dd` Thanks @mobeenabdullah! - Add a command palette to the page-builder editor. Opens on
mod+k, searches the commands the host - supplies, and runs the one chosen. Commands are data rather than built in, so the palette holds the
- keyboard surface and the host keeps its own vocabulary.
- #754 `51ddce0` Thanks @mobeenabdullah! - point the builder dev watchers at
src, sopnpm devrebuilds again
tsup --watch defaults to watching ., and at that root it never notices an
edit: no Change detected, no rebuild, and an artifact byte-identical
afterwards. Measured back to back on tsup 8.5.0 — --watch . saw nothing,
--watch src detected the same edit and rebuilt it.
Nothing errored while it was broken, which is why it survived: the watcher logs
a successful initial build and then Watching for changes, and the only symptom
is the ABSENCE of a later build line in output that scrolls. Anyone debugging a
stale dist was debugging code that had never been rebuilt.
- #733 `24a3a4d` Thanks @mobeenabdullah! - add the page-builder editor shell
@nextlyhq/builder gains BuilderShell — the editor frame: an icon rail, one
switched left panel, the canvas slot, a fixed right inspector, and the bars
around them. Presentational by contract: it owns which panel is open and the
region widths, and owns nothing about the document, so selection arrives as a
prop.
Also exported: the shell's own decisions (LEFT_PANELS, PANEL_BOUNDS,
RAIL_WIDTH, MIN_SHELL_WIDTH, MIN_CANVAS_WIDTH) and the PreferenceStore
port a host implements to keep chrome preferences wherever it already keeps
preferences.
New subpath @nextlyhq/builder/styles.css carries the --nx-builder-* chrome
token layer. A consumer that renders the shell without importing it gets
unstyled markup.
- #682 `7b19d8a` Thanks @mobeenabdullah! - Add the op store's vocabulary and inverse derivation to the builder: every document change is one of four id-addressed ops, and the op that undoes it is derived from the state it was applied to rather than declared by the caller.
- #831 `7a23525` Thanks @mobeenabdullah! - Rank every canvas drop target on one collision scale, so which target claims the pointer no longer depends on how deeply the page nests.
- #813 `a6555f8` Thanks @mobeenabdullah! - Stop the page-builder canvas reflowing when a drag starts. Drop zones no longer grow from zero to six pixels on dragstart, so blocks stay where they are while you aim, and the insertion bar now paints above blocks that carry a stacking context of their own.
- #781 `cec9cc3` Thanks @mobeenabdullah! - Updating a field group now changes its table wherever the update comes from. The mounted PATCH route and the Direct API previously stored the new fields without moving the physical schema, so only the admin panel performed the whole operation. A companion-table transition that fails now refuses the update instead of recording it as done, and the Direct API can toggle a field group localized.
- #795 `faf7fd7` Thanks @mobeenabdullah! - Add
core/columnas a real block and restrictcore/columnsto accept only columns, so a column can carry its own width, background and alignment.
A block whose slot refuses it is now reported by the repair banner and repaired by WRAPPING it in the one type the slot admits, so a page stored with loose children in a columns row can be fixed without discarding them. The block library's Insert button applies the same drop rules a drag does, inserting into the nearest place that accepts the block and reporting when there is nowhere. Slots declare whether they lay their children out with flex or grid, so the canvas stops interleaving drop zones that would become cells of that layout.
A block can declare the parents it may sit under — parent, matching the field of the same name in Gutenberg's block metadata — enforced on the editor and the write path alike, with the repair banner offering to wrap a stray block in the parent it names. This is the half a slot's allow list cannot express: a slot naming a type must not confine that type to it, and a block that is meaningless outside one parent has to say so itself.
It is declared on @nextlyhq/blocks-engine's BlockDefinition, so it reaches plugin authors through @nextlyhq/plugin-sdk/blocks alongside every other block field. A contributed block's nesting rules are enforced wherever the engine registry is populated — the write validator, the repair finder and the node constructor resolve a block's slots and permitted parents through it when this package's own registry does not hold the block. Not yet in the browser editor: blocks are registered by a plugin's server-side init, and the admin's client config transports only remotePatterns, so the browser realm's registry is empty and the canvas applies no contributed rule. Enforcement therefore holds at SAVE and not during editing, which is the safe direction — a document the editor let you build is still refused rather than stored — and it is a gap rather than a design. Slot allow-lists honour the engine's namespace wildcard (core/*) wherever they are read, rather than only exact names.
core/column uses parent so inserting a Column while one is selected produces a sibling in the row rather than a column nested inside a column.
blocks.manifest.json carries parent, and its manifestVersion moves to 2. That artifact is read by editor builds and by agents to decide where a block may legally sit, so omitting the field would not have made the restriction lenient — it would have told every reader there was none, and they would generate placements the write validator then refuses. The bump is required rather than cautious: the entry schema is strict, so a v1 reader rejects an entry carrying the new field outright.
The block library's Insert button now reaches a container's NAMED slot, not only default, so a container the drag path accepts is no longer refused by the click path. Documents are migrated when the editor loads them, which is what makes any block's migrate reachable at all — and migration only ever moves a document forward, never stamping an older definition version onto data written by a newer one.
The slot rules are now enforced in the editor's reducer, so paste, keyboard reorder and anything added later cannot write a document the save path refuses — previously only drag-and-drop consulted them. Documents are migrated when the editor loads them, which is what makes any block's migrate reachable at all.
Every drop target on the canvas now ranks by its depth in the tree, rather than only the zones between children doing so. A droppable that names no collision priority keeps the one its detector assigned — 3 with the pointer inside it, 2 otherwise — and dnd-kit compares priority before collision type and before overlap, so those targets outranked every zone shallower than that constant however the rectangles lay. The insert-before and append targets carried on each block were in that state, which put a nested container's own append target at or below the zones of the container holding it. They now read the same depth the zones do, so nesting decides which container claims a drop and geometry decides only where depths tie.
Fixes a crash opening an Image's aspect-ratio control: Radix refuses a select item whose value is the empty string.
- #766 `29e8129` Thanks @mobeenabdullah! - The field-group storage migration lock is now part of the schema Nextly reconciles. It was created on demand and declared nowhere, so it sat outside every migration: a change to that table could never reach an installation that already had one, because the statement that creates it does nothing to a table that exists. Nothing about the lock behaves differently today; what changes is that it can be maintained at all.
- #758 `fb9a0c0` Thanks @mobeenabdullah! - Show a disabled plugin's permissions on its detail page. They are seeded and
- granted whatever the plugin's enabled state, so withholding them made the page
- disagree with the database. Routes stay withheld — those genuinely are not
- mounted — and are disclosed separately as pending.
- #817 `5fc9cc7` Thanks @mobeenabdullah! - An email provider update no longer records a configuration change when a parser returns the same fields in a different order.
updateProvidercompared serialised text while the write path compares structurally, so a save that altered nothing could file a configuration-change entry in the activity log.
- #751 `e344e47` Thanks @mobeenabdullah! - The email delivery log is now bounded, and an erasure request survives a
- secret rotation.
The log records who was written to, identified by a digest of their address, and it grew on every send with nothing to remove it. The column that was meant to govern it and the index beside it were written and never read, so an operator reading a labelled retention class would reasonably have concluded something enforced it.
A sweep now removes rows past their window. It is offered by the SEND path rather than by a content write, because rows here are created by sends: that is when the table grows, a content write has no relationship to email volume, and an install that never sends mail carries no pass at all. Omitting the setting keeps a default window rather than keeping rows forever, since an unbounded record of recipients is not a reasonable default for a table an install fills without opting in.
This is the second half of erasure, and the halves cover different people. Erasing a named recipient only reaches someone a caller can name, and many recipients never had an account. The sweep reaches every row by age, whoever it belonged to.
Erasure also reached only rows hashed with the CURRENT secret. Rotating it left
older rows carrying a value the request no longer computed, so it matched
nothing and reported success — a privacy request that silently under-delivers.
Retired secrets can now be listed in \NEXTLY_SECRET_PREVIOUS\, kept for reading
and never for writing, and an erasure matches every digest those generations
could have produced. It accepts a comma-separated list for the ordinary case and
a JSON array for the secrets a comma-separated list cannot express — one holding
a comma or significant whitespace, \null\ for a generation that was unkeyed, and
\""\ for a secret that really was empty. Documented under "Rotating
\NEXTLY_SECRET\" in the environment reference.
Two things are deliberately unchanged. A send already in flight when a deletion commits still records its row; closing that would mean keeping a list of the addresses that asked to be forgotten, and the sweep bounds the row instead. And the retry columns stay inert: nothing drains this table, and a queue nobody drains looks durable without being so.
- #807 `8bb149f` Thanks @mobeenabdullah! - The Direct API now reports whether a field group is localized. A field-group update whose registry write fails after its companion table already changed is recorded with a new
divergedmigration status and reported as a change that stands, rather than raised as though nothing had happened.divergedis deliberately distinct fromfailed:failedmeans the table was never created and retrying is the repair, whiledivergedmeans the tables hold the new shape and the stored definition holds the old one, so the field group must be reconciled and the edit must NOT be retried. A diverged field group is refused for further schema edits until it is reconciled.
- #800 `7b23e26` Thanks @mobeenabdullah! - Updating a field group now refuses a field change that would need a column on its main table, pointing the caller at the schema preview and apply flow. Previously the request succeeded, recorded the new fields, and left the table without the columns it claimed to have.
- #745 `4c8d39c` Thanks @mobeenabdullah! - Retention no longer reads a sub-millisecond window as a request to delete everything.
A retention window is a whole number of milliseconds, so a fractional value is
rounded down. That rounding ran AFTER the check for zero, which meant any window
under one millisecond arrived as a window rather than as the zero it becomes:
\0.5\ was not zero when the check ran, and was zero by the time it was used.
On the audit trails a window of zero is treated as a mistake and replaced by the default, because erasing the record of who did what on a typo is not recoverable. That protection was reachable only by writing exactly zero. A value that rounded to zero skipped it and produced a cutoff of the current moment, which removes the entire trail on the next pass.
The rounding now happens before the reading, so a window is judged as the value it actually resolves to. A delivery ledger set to a fraction still keeps nothing, which is that trail's own position on zero and unchanged.
- #748 `a5ab500` Thanks @mobeenabdullah! - Label both ends of a date range, instead of relying on a placeholder that never renders.
A date input paints its own dd/mm/yyyy format hint and ignores placeholder outright, so a range written that way drew two identical empty boxes with nothing saying which end was which. The same spelling renders correctly on text and number inputs, which is why it survived: the defect is specific to one input type and invisible in the source.
Both date ranges in the admin -- the condition row and the entries filter menu -- now use one RangeField with real <label> elements bound to their inputs, and the pair is exposed as a named group. The filter menu had no accessible name on either input at all.
- #850 `9cdbbe1` Thanks @mobeenabdullah! - An interrupt during a legacy migration-lock claim now waits for the claim to settle before releasing it, so a shutdown no longer clears the row while the claim is still landing.
- #757 `d6f526e` Thanks @mobeenabdullah! - Give
DataTableViewapaginationprop and let the table place its own pager.
A pager's placement depends on whether the row table or the mobile card view is showing, and DataTableView is the only component that knows: the pager sits inside the card on desktop and takes the column's gap on mobile. Every list used to build the pager markup itself and hand it over, which left that decision at the call site — where the wrong arrangement is the one you get by writing the markup in reading order, and where several surfaces had drifted into it.
Tables now pass pagination as data: currentPage, pageSize, onPageChange and the rest, typed as the pager's own props rather than a restatement of them. A caller supplying state has no opportunity to place the control, so the mistake is no longer available to make. API keys, deliveries, webhooks, collections, field groups, singles, roles, users, plugins, email providers, email templates, image sizes, entries and the media list view are all on it, and MediaListView forwards the prop rather than a node.
Two surfaces keep rendering a pager directly, and say why where they render it: the media grid, which has no row-versus-card view to place one for, and the user-fields list, whose drag-reorderable rows are drawn by a DndContext over a plain table rather than by DataTableView.
Two fixes found along the way. Choosing a larger page size on the image sizes list left the page number pointing past the end, showing the empty message over a list that had rows. And the media library's two pagers now carry distinct accessible labels rather than both announcing themselves as "Pagination".
- #773 `7948d1f` Thanks @mobeenabdullah! - fix(create-nextly-app): keep pnpm add working in a pnpm scaffold
- #771 `fc92a4d` Thanks @mobeenabdullah! - Decide a boxed BigInt by its internal slot rather than by
Symbol.toStringTag, so a document cannot tag itself unstorable, and skip the whole-document serialization for a document the engine already refused as too large.
- #846 `f29ebeb` Thanks @mobeenabdullah! - A schema sync on a database whose migration-lock table predates its expiry column now holds that lock by owner instead of running without one.
- #777 `9a291fe` Thanks @mobeenabdullah! - The field-group migration lock now expires. A run renews its claim while it works, so a run that crashes or is killed no longer leaves a lock only an operator can clear, while a run that is still working keeps the lock for as long as it needs it. A run whose claim is taken over or can no longer be renewed fails loudly instead of continuing unprotected.
- #838 `b58f55c` Thanks @mobeenabdullah! - A schema sync now reports a migration lock it had to skip, and a run whose lock renewal never answers fails instead of hanging.
- #833 `a0e2817` Thanks @mobeenabdullah! - Make one control size name mean one control height.
size="sm" resolved to --nx-control-height-md (36px) on Button and --nx-control-height-sm (32px) on Input and SelectTrigger, so a small button beside a small input or select sat 4px out of line. default and lg already agreed; only sm diverged.
Input and select now take the same step as button. Nothing changes visually today: there was not one <Input size="sm"> or <SelectTrigger size="sm"> anywhere in the repository, which is why the divergence survived — it was waiting for its first call site rather than showing up on a screen. Aligning the other direction would have shrunk sixty live buttons to fix a case nobody had hit yet.
A test now calls the exported cva functions and asserts that every size name shared by these primitives resolves to the same height token, and that the steps stay ordered. It reads the class string a caller actually receives rather than parsing the variant maps out of the source.
The admin sidebar's search field asked for h-9 directly, which happened to equal the small step and then stopped tracking it. It takes size="sm" now, and its icon is centred rather than offset by a fixed top-2.5 that only centred inside a 36px control — the same height decision written a second time.
- #857 `224c729` Thanks @mobeenabdullah! - Declare the admin's session-free routes once.
Which routes are reachable without a session was answered in three places: the
page registry, a hand-kept set in the refresh interceptor, and the
pages/(auth)/ directory. A page added to the registry but missed in the
interceptor still rendered, but its expected 401 redirected to login and
discarded the URL, which is how an invite token was once lost.
PUBLIC_ROUTE_PATHS in constants/routes.ts is now the declaration. The
registry keys its public pages by that type, so the two cannot disagree without
failing the build, and the interceptor derives its set from the same array. A
test reads the (auth) directory, which no type can reach, and fails on a page
nobody declared. No behaviour changes.
- #743 `b55e278` Thanks @mobeenabdullah! - Retention now keeps what you asked it to keep.
Setting a retention window to Infinity — the strongest way the type allows you
to say "keep these forever" — was deleting instead. Audit trails were removed
after 90 days and webhook events after 30, on the schedule the default sets,
while the setting itself read as accepted. Nothing surfaced it: the pass ran,
reported success, and pruned rows the configuration had asked to retain.
The cause was two separate answers to one question. Audit and webhook retention each resolved a configured window in their own file, and the two had drifted: a 2000-year window kept everything, an infinite one deleted, and the same input produced different outcomes depending on which trail it was written for. Webhook retention also had no upper bound at all, so a very large window produced a cutoff date no database column can store, which made the pass fail silently on every run and leave the ledger unpruned.
There is now one resolver behind both, built on the rule they disagreed about: refusing a value must never delete more than accepting it would. An infinite window, and any window longer than a date can express, now mean keep forever. Values that ask for less than the default, or for nothing coherent, still fall back to the default, because that direction cannot lose data.
How long a window a trail can express is stated by the trail rather than shared, because it is set by the column the cutoff is compared against and those differ. Content activity is compared against a column counting from 1970 and so tops out around fifty years; the audit, event and delivery trails count from a calendar year and accept far longer windows. Sharing one ceiling would have meant a window a column can hold being answered with "never prune", which is unbounded growth on a setting that asked for the opposite.
Two positions each trail holds on its own are unchanged: false still means
keep forever everywhere, and a delivery ledger set to zero still keeps nothing,
which is a real choice for a table whose only purpose is making a retry
possible.
- #779 `332d56e` Thanks @mobeenabdullah! - Write a block node's own fields in the declared order when an op rewrites it, so undoing a removed field restores the document rather than only its values.
- #856 `f7545fe` Thanks @mobeenabdullah! - Disclose a plugin's retired permissions on its detail page instead of omitting
- them.
The permission list endpoint now forwards includeOrphaned, so a caller that
reports what a plugin owns can ask for rows nothing declares any more. They are
shown marked rather than hidden: the row still exists and still carries its
grants, so leaving it out understated what a plugin left behind. Lists that
OFFER permissions are unchanged, because the option is off unless asked for, so
the role permission matrix still shows only permissions that enforce something.
- #809 `e19f31a` Thanks @mobeenabdullah! - fix(nextly): persist the admin options a collection is allowed to set
order and sidebarGroup were accepted by CollectionAdminOptions and dropped by the projection that writes the registry, so a code-first collection could set its sidebar position, type-check, and still sort by the default. admin.description had no column under admin at all; it now resolves to the collection's own description, which is the field the admin already renders and the Schema Builder already edits.
A compile-time assertion now requires every admin option to be either persisted or listed with the reason it is not, so adding one forces the author to classify it in the same change. That list is exactly what drifted twice before.
- #747 `c92db86` Thanks @mobeenabdullah! - Reject duplicate plugin admin slugs at boot.
pluginAdminSlugcollapses every - non-alphanumeric run to a single dash, so distinct package names can map to one
- slug and the plugins then share a single admin address — one plugin's detail
- page opens the other's, and host
pluginOverridesapply to the wrong package. - No lookup downstream can detect this, because every lookup along that address
- returns a plugin.
resolvePluginsnow refuses to start, naming both packages - and the slug they collide on.
- #762 `e24638c` Thanks @mobeenabdullah! - Warn at boot when a plugin ships without an
admin.description. Without one the - admin can only show the package specifier wherever it lists that plugin, and
- nothing previously stopped a plugin shipping that way.
- #749 `2f2f089` Thanks @mobeenabdullah! - Give the installed plugin detail page a two-column layout with a sticky
- metadata rail. About moves into an aside beside the contributions rather than
- below them, so what a plugin adds — its permissions and API routes included —
- stays visible while its metadata is read.
- #742 `d4f6480` Thanks @mobeenabdullah! - The admin now has a plugin directory, at Plugins then Browse plugins.
It lists the plugins Nextly publishes with a description, category and author, marks the ones already installed, and searches by name, description and tags. A curated row sits above the grid while there is more in the grid than in the row.
It is discovery only. Installing a plugin means adding a dependency and a line to nextly.config.ts, so the directory never writes to your source or changes plugin state. Where a listed plugin is already installed, its own icon and description are shown rather than the directory's copy of them.
- #753 `85d526e` Thanks @mobeenabdullah! - Disclose the routes a disabled plugin would serve once enabled. A disabled
- plugin mounts no routes, so
routesstays empty and the same declarations - travel as
whenEnabledinstead — only those that would actually mount, checked - by the same fold that mounts them. Its permissions are untouched by this: they
- are seeded whatever the plugin's enabled state, so they were never pending on
- anything.
- #826 `f0b9f1d` Thanks @mobeenabdullah! - Show a plugin's permissions on its detail page again, read from the
- authenticated permissions endpoint rather than the public admin-meta payload.
These are the rows the seeder actually created, which is a different set from
the declarations: a publish or unpublish declaration naming a collection
or single is dropped, because the seeder emits that slug itself and keeps the
row ownerless. The page now reports what exists rather than what was asked
for, and it reports nothing at all when the request fails instead of showing
an empty section.
- #842 `4fdbf77` Thanks @mobeenabdullah! - The entry editor now offers Copy shareable link.
The preview-link machinery already shipped — a mint route gated by update, an admin service, a usePreviewLink hook and the PreviewActions control — but nothing in the standalone editor rendered any of it: the control was wired only into the form footer, which the editor renders in embedded (modal) layouts alone. An author had no way to reach the feature.
The control now sits in the editor's action bar, directly left of Save, for a saved entry whose author holds update on the collection. The permission half of that condition is resolved by the header itself rather than by each caller, so the gate cannot be omitted by a future call site.
- #845 `1b0689e` Thanks @mobeenabdullah! - Serve only branding from the public
/api/admin-meta. Plugin contributions, configured locales, custom sidebar groups and builder availability now come from the session-gated/api/admin-meta/workspace, so a plugin-declared permission slug is no longer readable before sign-in. The admin reads both and merges them, so no component changes.
- #823 `5244934` Thanks @mobeenabdullah! - Stop serving plugins' declared custom permissions on the public
-
/api/admin-metapayload. That endpoint answers without authentication, so - every plugin action and resource name it carried was readable by anyone who
- could reach the app.
The plugin detail page no longer lists a plugin's permissions. Reading them from an authenticated endpoint is a separate change and is not in this release.
- #738 `2f3bb57` Thanks @mobeenabdullah! - The block document format now publishes a JSON Schema, so a generator, an editor
- build or an agent can check a document against the format without TypeScript.
- #737 `791a08e` Thanks @mobeenabdullah! - A field-group storage migration dry run no longer writes anything. It observes the migration lock instead of claiming it, so a preview works with a read-only database role, and reports what it could learn about the lock as
lockon the dry-run outcome rather than refusing when another run is in flight.lockis{ kind: "held", owner },{ kind: "not-held" }or{ kind: "unknown", reason }— an unreadable lock table is reported as unknown rather than as nothing holding the lock.
Because a preview takes no lock, another run can advance between its reads and leave it scoring the plan against a state the database was never in. A dry run now re-reads and retries when that happens, and the outcome carries basis to say which answer it ended up with: { kind: "reconciled" } when the plan was scored against the live catalog, or { kind: "unreconciled", reason } when a writer kept moving underneath it. An unreconciled preview still reports every rename the migration declares rather than an empty list, so it can never be mistaken for "nothing to do". Refusals that re-reading cannot clear are ultimately preserved: a torn-shaped but persistent conflict now spends its attempts confirming the database is not moving before the refusal stands, so a conflicted database sees the extra catalog reads that stability check costs.
- #789 `0b3fc78` Thanks @mobeenabdullah! - Fix the scaffold job's workspace-package pin, and fail closed on an unreadable
- search-index manifest.
The pin rewrites dependency specifiers after the scaffold has generated its lockfile, and pnpm turns frozen-lockfile on by default in CI — so the pnpm blog leg aborted with ERR_PNPM_OUTDATED_LOCKFILE before it could build.
An index manifest that exists but cannot be parsed no longer reads as owning nothing. writeFileSync is not atomic, so an interrupted build can truncate it, and treating that as an empty ownership list left the previous index in place while the status flipped to empty — the search page would load and serve unpublished results.
- #791 `20c1d43` Thanks @mobeenabdullah! - Stop generating a
db:migrate:resetscript that names a command the CLI does not - register. Every scaffolded project shipped an
npm run db:migrate:resetthat - failed;
db:migrate:freshalready drops all tables and re-runs the migrations.
- #759 `e520db5` Thanks @mobeenabdullah! - A Schema Builder change to a single or a field group now holds the field-group storage migration out for its whole duration, rather than being able to start one halfway through. The exclusion is taken before the change plans anything, so a create, an update or a delete either runs against storage nothing is renaming or is refused
- outright — and a change that is refused has written no row and built no table of its own. Taking
- the exclusion can still create the migration lock's own table, which is empty, holds no content,
- and would have been created by the next successful change anyway. A database that has never run a migration is covered too: these paths may create the lock table, so a first migration cannot claim it and start renaming underneath a change already in progress.
Not every way of changing schema is covered yet. The Admin's confirmed apply, the standalone schema routes, collections and user fields still write without the exclusion, so they can run alongside a storage migration.
- #801 `d9bbcf6` Thanks @mobeenabdullah! - Toggling a field group between localized and not now advances its schema version, so a Schema Builder tab opened before the change is told to reload instead of overwriting it. Previously only a field change advanced the version, and the toggle moves columns between tables.
- #739 `b09b087` Thanks @mobeenabdullah! - Make the admin search field an
Inputrather than a second implementation of one.
SearchBar restated Input's classes instead of composing it, and the copy had drifted twelve ways: no aria-invalid or data-[invalid=true] handling at all, so a search field could not show an error state; focus:border-primary without the ! Input uses; and no selection:* colours, placeholder:opacity-50 or disabled:pointer-events-none. Palette work reached every input except this one, because the border token was named in two places and only one was maintained.
The field is also type="search" now, so assistive technology announces it as one.
Its className reaches the wrapper, not the field, so the border-input and border-border classes eighteen call sites passed were inert. Those are removed, and in development the component now names any it receives so the next one is visible rather than silent.
That warning judges the class string the element actually receives, and only reports a class that does nothing on the box as rendered: give the wrapper a border and a border colour paints, give it padding and a background shows around the field, and in each case the class is left alone.
Input also sets its own text colour now. It set one for file inputs and for placeholders but never for the field's own text, so it inherited whatever surrounded it — which Tailwind's preflight resets to inherit on form controls.
- #761 `7133efb` Thanks @mobeenabdullah! - Load template and playground fonts from packages instead of fetching them from Google Fonts during the build.
- #784 `eefb655` Thanks @mobeenabdullah! - Give a list's page state one implementation.
Thirteen places in the admin held the same two lines: set the page size, return to page one. The copy that drifted meant choosing a larger page size from a later page asked for rows past the end of the list, and the table rendered its empty message over a list that had rows.
usePagination owns page and size together, so the resets travel with the state rather than with each caller: a size change returns to the first page, and resetPage covers a search or filter change that alters which rows exist. Both settings move in one update, so a query keyed on them refetches once rather than once per setter. useServerTable derives from it rather than restating it.
- #767 `9a8d259` Thanks @mobeenabdullah! - fix(create-nextly-app): declare
packagesin the generated pnpm-workspace.yaml so scaffolded projects install on pnpm 9
- #770 `dd3eafd` Thanks @mobeenabdullah! - fix(create-nextly-app): ship the template .gitignore through npm packing, so a new project does not commit its .env
- #797 `ec9b4c7` Thanks @mobeenabdullah! - Bound block-document validation by the limits the survey enforced, so a caller passing a limits object whose values change between reads can no longer make the walk outrun the cap that was checked.
- #721 `a398047` Thanks @mobeenabdullah! - Tabs now look the same everywhere.
The admin's tab strips are an underline control: the active tab is marked by a bottom border, and the tab is square so that border runs flush to its edges. The shared component already draws all of it — the underline, the active and hover colours, the focus ring.
Several first-party plugin screens were drawing their own instead. The form builder switched the underline off and repainted it from React state through an inline style, three field-editor tabs restated the whole indicator, and a few places re-declared a square corner the component already guarantees. The result was the same component wearing a different appearance depending on the screen.
Those screens now pass layout only and let the component draw the indicator, so the page builder's inspector, the form builder, its field editor, its preview and its submissions list all match the rest of the admin. Layout overrides stay allowed, because a tab strip in a dialog is a different shape from one in a sheet.
A test reads every first-party call site and reports one that repaints the indicator, so the next screen to do it is caught in review rather than noticed later. It reads what a call site is written as, which is not the same as guaranteeing the appearance cannot be forked: a class arriving from another module, through a prop spread, or through a slotted child is not something it can see. The component stays deliberately overridable so a theme can move these values, and that is the same door a call site can walk through.
- #821 `d011d54` Thanks @mobeenabdullah! - Render a table's custom footer beside its pager rather than instead of it.
DataTableView resolved its footer slot as pagination ? pager : footer, on the reasoning that two pagers in one slot is not a composition anyone wants. But footer takes an arbitrary node rather than a pager: a caller using it for a selection summary or bulk actions and then adopting pagination lost that content, with both props public, both permitted by the type, and nothing reporting the loss. Both render now, footer first, since a summary describes the rows above it and the pager moves between them.
Also removes a comment in the media library that explained the grid pager's accessible label by what a source-level placement guard needed. That guard was deleted in the same release, so the comment described nothing; the screen-reader reason is the real one and is kept.
- #828 `e5e4023` Thanks @mobeenabdullah! - Tabs gain
TabsList variant="ghost"andTabsTrigger size="sm", so the compact tab appearance is named rather than spelled out inclassNameat each call site. The two call sites that hand-rolled the ghost list disagreed on its height (h-8andh-7); the variant settles it ath-8.
- #778 `d3e487a` Thanks @mobeenabdullah! - Store the configuration a provider parsed, and refuse a write whose parse is not a fixed point.
The service persisted whatever the caller submitted while the adapter closed over the parse result, so every difference between the two became a defect somewhere that read the row. It now persists the parsed value, and checks before writing that parsing the stored form returns the stored form -- rejecting a parseConfig that derives a credential, returns a value JSON cannot carry, or refuses its own output, each of which would otherwise hand the adapter a configuration nobody saved.
- #804 `a88d6c5` Thanks @mobeenabdullah! - Ignore the config copies tsup writes for a package that builds more than one bundle. A watcher stopped with Ctrl-C left a tsup.<name>.config.bundled\_\*.mjs behind that no ignore rule covered, and the next lint failed with a parsing error naming a file nobody wrote.
- #750 `36825d4` Thanks @mobeenabdullah! - Start both build watchers of
@nextlyhq/uion every platform. Thedevscript used a POSIX - background-and-wait, which
cmd.exeruns sequentially, so on Windows the first watcher held the - line and the server-safe artifacts were never rebuilt — with no error, no exit code and no output.
- #741 `02ade17` Thanks @mobeenabdullah! - Convert
packages/ui's build scripts to TypeScript and delete the hand-written - declaration files beside them. Nothing kept a
.d.mtsin step with the module - it typed, so a test compared the two — and that comparison had to model every way
- ECMAScript can publish a name. There is no second list to drift now, and the
- scripts are type-checked for the first time.
- #803 `40dfd52` Thanks @mobeenabdullah! - fix(nextly): paginate users by user rather than by role-joined row
listUsers applied LIMIT/OFFSET to a query that left-joined user_roles and roles and grouped afterwards, so a user holding three roles consumed three rows of the page. A page of N therefore returned fewer than N users, and OFFSET advanced over joined rows rather than users — which skipped users entirely rather than merely short-filling the page. Measured on nine users with two holding three roles each: walking every page visited six of them.
The page query now selects one row per user and roles are fetched for exactly the users that page selected, so total keeps counting the same thing it always did and a page of N contains N distinct users. Role order per user is now deterministic; the join left it to the planner.
- #799 `5ff805e` Thanks @mobeenabdullah! - Add validateDocument, which returns the survey a validation judged a block document with, so a caller can ask whether the engine measured it in full instead of inferring that from issue codes. validate keeps its signature and becomes the narrow view over it.
- #799 `5ff805e` Thanks @mobeenabdullah! - Report which of three things JSON does to a block document instead of one flag for all of them. A document JSON writes but rewrites - an array hole, a dropped key, a negative zero - is no longer refused as having no stored form, and a document the validator declined to read is reported as unmeasured rather than as unwritable.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/blocks-react@nextlyhq/builder@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 19 packages at 0.0.2-alpha.57 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #714 `5673fff` Thanks @mobeenabdullah! - The admin now ships with rounded corners and the Geist typeface. Corner radius comes from a single
--radiusknob, so changing that one declaration re-rounds the whole panel, and a plugin built against the published Tailwind preset re-rounds with it.
- #699 `6936078` Thanks @mobeenabdullah! - Add an experimental BreakpointDialog to @nextlyhq/ui, with the validation behind it. The style compiler discards a breakpoint it cannot use rather than raising, so a bad definition is lost silently and surfaces later as stale styles; the dialog refuses to save any set that would lose one.
- #728 `38e5e6b` Thanks @mobeenabdullah! - move the breakpoint editor into the builder, where its rules can be derived
lib/breakpoints.ts and breakpoint-dialog.tsx restated the style compiler's
breakpoint drop rules because @nextlyhq/ui is the block-agnostic layer and
cannot depend on @nextlyhq/blocks-engine. Two implementations of one rule
agree the day they are written and drift silently after.
They now live in @nextlyhq/builder, which already depends on the engine and
imports MAX_BREAKPOINTS_PER_AXIS and the breakpoint types from it rather than
mirroring them.
Breaking, and deliberate: the @nextlyhq/ui/breakpoints subpath is removed,
along with BreakpointDialog and the breakpoint types from the root barrel.
Nothing in this repository imported them, and every affected export was
@experimental.
- #683 `5bfac2f` Thanks @mobeenabdullah! - Add the builder's host-canvas coordinate mapping: one module converts between the canvas frame and the host page, including the scaled border inset that places the frame's content origin. A sibling test scans for cross-frame rectangle reads elsewhere in the package, recognising a bounded set of spellings; it narrows the paths taken by accident rather than enforcing single ownership.
- #717 `5a05e7b` Thanks @mobeenabdullah! - Add an experimental ColorPicker to @nextlyhq/ui, with the pointer-to-colour geometry behind it on the server-safe @nextlyhq/ui/color entry. The picker knows nothing about design tokens: a swatch carries an opaque value it hands back untouched, so a host storing a token reference keeps it rather than receiving the colour that token happened to resolve to.
- #713 `dbd95b3` Thanks @mobeenabdullah! - Erase a recipient from the email delivery log.
Deleting a user left their delivery rows behind carrying a keyed hash of their
address, which an install holds the key for, so the table went on answering
"was this person written to, and when" for an account that no longer exists.
eraseRecipientDeliveries overwrites that hash with a value no address can
produce, keeping the row, its status and its timing so aggregate questions
still have an answer. deleteUser calls it inside its existing transaction, so
a failed erasure takes the deletion with it rather than leaving the two out of
step.
The erasure takes an ADDRESS rather than a user id, because most recipients
never had an account: a password reset to an address that never registered, a
CC, a BCC added by a beforeSend filter. Those people can ask to be erased too
and no account deletion will ever fire for them, so it is callable directly.
EmailDeliveryRecord.recipientHash is now string | null, where null means
erased.
- #734 `193d5ec` Thanks @mobeenabdullah! - Advertise the Node range this project actually supports. Every package declared
-
>=20.0.0while the repository requires^20.19.0 || ^22.12.0 || >=24.0.0, so - installs on 20.6-20.18 or on 23.x succeeded without warning and failed later at
- runtime. Release preflight now derives the expected range from the root manifest
- and rejects a package that disagrees, so the two cannot drift apart again.
- #722 `696281d` Thanks @mobeenabdullah! - Field group instances now report their stored type through
nextly/field-group-type, a new entry point that reads whichever spelling a document carries and writes the current one. The admin editor uses it, so content saved before and after the storage rename stays readable and selectable in both.
- #700 `cf04a67` Thanks @mobeenabdullah! - Correct the frame content origin to include the iframe's padding, and measure that inset in one place.
An iframe's nested viewport begins at the content box, so padding displaces it exactly as a border does. Callers built the inset from clientLeft/clientTop, which report the border alone, so every frame-local point mapped toward the border by the scaled padding. frameInsetOf is now exported as the single reader, and both the README recipe and the FrameGeometry documentation name it instead of restating arithmetic three call sites had already got wrong.
- #689 `213a860` Thanks @mobeenabdullah! - Admin list pages now attach their pagination to the table it belongs to, instead of leaving it floating a row below the table on some pages and attached on others. Applies to users, plugins, roles and webhook endpoints.
- #725 `73885c6` Thanks @mobeenabdullah! - The field-group storage migration can now report what it would rename without changing any content or recording that a run happened, and refuses to run for real unless the caller states that a restorable backup exists. A preview still claims the migration lock, so it needs a role that can write to Nextly's own lock table.
- #719 `f61172e` Thanks @mobeenabdullah! - A stylesheet stored for a page is no longer reused when a block migration has
- since turned one of its nodes into one that renders nothing. The rules compiled
- for that node, and any image the rules fetched, were still being served for
- markup no visitor receives.
- #730 `6683ef3` Thanks @mobeenabdullah! - Plugin icons now resolve through one shared rule, so the same plugin shows the same icon everywhere in the admin, and a plugin can ship its own logo image instead of naming a built-in glyph.
The SEO plugin now describes itself in the plugins list instead of showing a bare package name.
A styling fixture used only by the end-to-end suite no longer appears as an installed plugin, and no longer injects a showcase section into the Posts collection list, in a normal development server.
- #740 `db7122d` Thanks @mobeenabdullah! -
@nextlyhq/plugin-sdknow exportspluginAdminSlug,PLUGIN_CATEGORIESandisPluginCategory(experimental), so a plugin author can derive a plugin's admin slug and check a category against the vocabularydefinePluginaccepts, rather than reimplementing either. They are also onnextlyandnextly/configfor host apps.
The admin uses those exports instead of its own copies. It previously derived a plugin's URL slug with its own implementation of core's algorithm, so a plugin page could be linked at one slug and routed at another the moment either side changed, and it kept its own list of valid categories, so it could reject a category definePlugin accepts.
Nothing changes in the admin UI. The plugin directory that consumes these is not built yet; this is the groundwork it needs.
- #727 `53fca3e` Thanks @mobeenabdullah! - On desktop, the Plugins item in the admin sidebar now opens the plugins page when you click it, instead of only expanding the sub-sidebar and leaving you to find the page yourself. On mobile it still opens the panel, as every sidebar section with a panel does, and Installed Plugins is the first entry inside it. The item also stays visible when no plugins are installed, so a new project can reach the plugins page at all.
Users who can read a plugin's collections but cannot manage settings keep the sub-sidebar, since the plugins page itself is settings-guarded.
The secondary sidebar now closes when the category it is showing stops being one of the sidebar's destinations, so a slow or failing permissions load no longer leaves an empty panel open beside the page.
- #671 `75054a8` Thanks @mobeenabdullah! - Relationship expansion can now be told WHICH collections a trusted read may
- reach, judged per expansion target.
overrideAccess says the caller is trusted. It said nothing about the
collection a relationship points at — which the caller never named and may not
serve to the same audience — so a trusted read spread that trust into every
target it populated. A caller serving one fixed audience can now state its
trusted set, and anything outside it is read as that audience would read it.
Absent the new option nothing changes, so the Direct API keeps its semantics: a caller that has already decided who is asking is not narrowed by a default it never chose.
- #724 `35ff30a` Thanks @mobeenabdullah! - A page whose stylesheet is reused now keeps it when a block migration turns a
- condition-gated node into one that renders nothing. Those nodes never had rules
- in the shared sheet, so withholding it cost every other block on the page its
- styling.
- #673 `67082d1` Thanks @mobeenabdullah! - Check the built server-safe entry points against what the build recorded, and stop publishing the
- bundler metafiles those checks read.
The gate reads two records the build already wrote — the module specifiers surviving in each artifact and every chunk reachable from it, and the bundler's own metafile of what it inlined. A bundled dependency leaves no import to find, so the text alone cannot answer what an artifact reaches. The metafiles are build inputs to that check rather than something a consumer needs, so they are excluded from the published files.
- #702 `8011731` Thanks @mobeenabdullah! - fix(ui): ignore a dispatched event that is not a keystroke
The shortcut manager listens on document, so every event dispatched anywhere on
the page reaches it — including synthetic ones from code outside the application.
A password manager typing into a credential field dispatches a keydown carrying
no key, and the manager spread it as a string, crashing the page with
TypeError: key is not iterable. It now ignores an event it cannot read as a
keystroke, and leaves it propagating to whichever listener does understand it.
- #697 `ca1cc48` Thanks @mobeenabdullah! - Carry a trusted write's bound into a Single's upload expansion. A Single holding uploads and no relationship field returned whole media rows in its write response, because the bound reached only the relationship expansion beside it, which returns early for such a document.
- #705 `ecefaa2` Thanks @mobeenabdullah! - A field group instance now reports its type whichever spelling the stored document uses, so content written before and after the storage rename both read. A
wherefilter on the type keeps working under either spelling, and version snapshots keep recording the type of components nested inside a dynamic zone. Reading that type is one shared call rather than a key spelled out at each site, which is what keeps the rename a change in a single place.
- #716 `cf48bd7` Thanks @mobeenabdullah! - A version snapshot now records each field group instance under one spelling of its type key. An entry captured before the storage rename, restored, and captured again previously kept its old key alongside the new one, so the snapshot announced the same instance's type twice.
- #731 `298d41e` Thanks @mobeenabdullah! - Page builder inspector: keep the open panel tab in sync when the selected block changes type, so the inspector no longer shows a tab the block does not have.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/blocks-react@nextlyhq/builder@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 19 packages at 0.0.2-alpha.56 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #633 `175ed53` Thanks @mobeenabdullah! - admin: render the email provider form from the server's provider descriptors
The provider form no longer knows any provider by name. It fetches the registered types and their field metadata from the server and builds the picker, the controls and the client-side validation from that, so a provider contributed by a plugin is configurable in Settings without editing the admin.
Dotted field names are treated as paths, so a provider declaring auth.pass
stores { auth: { pass } }, and a credential the user did not touch is
omitted from the update rather than overwritten with the mask that stood in for
it. A provider whose plugin has been removed renders read-only with its type
named instead of as a blank form.
Also fixes the Active toggle on the edit page, which was rendered and then left out of the update payload, so pausing a provider silently did nothing.
nextly: record who created, changed, promoted or deleted an email provider
email_providers holds the credentials that send password-reset and
verification mail, so an actor who can edit a provider can point every
authentication email at a relay they control. That action previously left no
record. Create, update, delete and promote-to-default now write an activity
entry naming the actor, the provider and which fields changed.
Names, never values: an entry carries no part of the configuration, and a
configuration change is recorded as the single field name configuration
rather than by its inner paths. An update that moved nothing writes no entry
at all.
The provider screens also tell a catalog that could not be loaded apart from one that merely could not be refreshed. A failed refresh keeps the descriptors already fetched, so the type filter, the row labels and the form all still work from them; the pages now say so instead of reporting the catalog unavailable, and the edit page's Update button follows the form into read-only when the cached catalog no longer lists the stored type.
Promoting a provider to default is one transaction. The demotion of the previous default and the write that promotes previously committed separately, so a promotion that matched nothing — a row deleted between the read and the write, an insert the database refused — left the installation with no default provider at all and nothing in the trail to say why.
Inside that transaction the demotion runs first. PostgreSQL carries a partial
unique index over is_default = true and checks it as each statement runs, so a
row taking the default while the incumbent still holds it is rejected outright.
A promotion that then matches no row — because the provider was deleted in the
meantime — throws rather than commits, which takes its own demotion back with
it.
A masked value is no longer written back over what it stood for. The read masks a configuration path the provider does not describe — a credential left behind by an upgrade, say — while the write stripped masks only from paths declared secret, so a client echoing the configuration it was given replaced the real stored value with eight bullet characters during an unrelated edit. Masking and unmasking now ask one question.
Only a handover opens a transaction. Wrapping every provider write in one cost
correctness on SQLite, where the transaction is BEGIN IMMEDIATE on a single
shared connection: a second ordinary write arriving while the first was open
could not begin at all.
An edit form left open reconciles a newer version of the record it is showing. The detail query refetches on focus, so a change made elsewhere used to be held and written back on the next save, reverting it from an edit that never touched those fields. Fields the operator has touched keep what they typed. If the record's TYPE changed, the configuration is rebuilt from the new provider rather than carried across — otherwise one provider's credential is submitted as another's wherever both declare the same field name.
A stored value that predates a tightened constraint no longer blocks unrelated
edits. A provider upgrade that lowers maxLength, or narrows a numeric range,
made every provider holding an older value unrenameable and undeactivatable. The
provider's own parser stays the authority on what it accepts; the descriptor
governs replacements.
Provider metadata that no descriptor can publish is refused at registration
rather than at the first request for the catalog: options that is not an array
of { value, label } on any field kind, two select options sharing a value, and
capabilities given as an array. One malformed provider previously took the
whole catalog endpoint down, and with it every provider's form.
- #653 `3709979` Thanks @mobeenabdullah! - Route the admin panel's keyboard shortcuts through the shared shortcut manager, so one listener owns every key and precedence follows the component tree rather than mount order.
- #644 `80ca19e` Thanks @mobeenabdullah! - Refuse an unknown URL scheme in a block's attributes instead of naming the dangerous ones.
The guard every block prop that reaches an href or a src passes through was a BLOCKLIST: javascript:, vbscript: and data: were named and refused, and everything else was allowed. So blob: was allowed — and a blob: document runs in the origin that created it, which is the page's own. So were filesystem:, about:, view-source:, and whatever a browser ships next. A blocklist has to predict every dangerous scheme and misses the one nobody had heard of when it was written, which is the same reason the style compiler and the remote-host policy are both allowlists.
Four schemes are accepted now: http and https for a destination, mailto and tel for the two that open an app rather than a page and are the ordinary content of a contact button. A value carrying no scheme is untouched, so /about, a.png, #top and //cdn.example/a.png all still work — which hosts may be REACHED is a separate question, asked of the host policy by the blocks that fetch rather than of a list of schemes.
These are the same four the rich-text sanitizer already allows, and that is deliberate rather than a coincidence: it answers this identical question for stored rich text, and two surfaces of one product disagreeing about which schemes are safe is how a value refused inside a link body becomes acceptable in a button beside it. The admin's link editor keeps accepting a wider set for what an author may TYPE, because that is an input affordance and not the boundary.
The scheme is read from the value as the browser's parser will read it, and through the ENGINE's normalisation rather than a second copy of the rules — two spellings of one algorithm disagreeing is how a scheme hides from a check while still navigating. Tab, LF and CR are removed wherever they appear because the parser removes them; leading control characters and spaces are trimmed because the parser trims them.
An interior space is deliberately NOT removed, because the parser does not remove one either — it percent-encodes it. hero image:1.png is an ordinary relative path to a file whose name holds a space, and collapsing that to heroimage:1.png would invent a scheme nobody wrote and refuse the path. A control character still sitting inside the value after normalisation refuses it outright instead: one never appears in a URL anybody meant, since it has to be percent-encoded to survive, and its only use here is to split a scheme so a reader sees none where a browser may still see one.
The value returned is still the original trimmed string, so a legitimate URL is never silently rewritten.
- #643 `07cd50f` Thanks @mobeenabdullah! - Page validation now refuses children stored under a slot on a block that holds none, not just on containers with the wrong slot name. Every block in the catalogue declares its structure where the check can read it without loading the block library.
- #636 `b4e032b` Thanks @mobeenabdullah! - Page validation now knows what slots a block declares without the block library having to be loaded, so a page saved through the normal server path is checked rather than waved through. Three layout blocks move to the new source in this change; the rest follow.
- #640 `19f35d9` Thanks @mobeenabdullah! - Every block that can hold children now declares its slots where page validation can read them without loading the block library, so a page saved through the normal server path is checked against all of them rather than a few.
- #691 `8f5d785` Thanks @mobeenabdullah! - Type-check
blocks-engine's test files, and stop Node globals reachingsrc.
Turning the check on surfaced a real defect in the published types:
AnyBlockDefinition widened every prop-consuming member except seo, so
registerBlocks rejected every definition built by defineBlock<P>() for any
interface P without an index signature — whether or not it contributed SEO.
seo is now widened like its siblings, so typed blocks register.
- #662 `18b529b` Thanks @mobeenabdullah! -
@nextlyhq/blocks-reactnow emits a prepared document's slots in the order the - block DEFINITION declares them, not the order they happen to be stored in.
The renderer asks for its slots by calling renderSlot once per declaration,
so declaration order is the order the page presents. This tree is documented as
the render-equivalent one, so carrying stored order left its own key order
describing a page nobody is served, and made two documents that render
identically compare as different.
A slot the definition declares but the document never stored stays ABSENT rather than being added as an empty array: an empty slot renders nothing either way, and adding it would rewrite every document that omits an optional slot.
- #687 `e1d573e` Thanks @mobeenabdullah! - The page renderer and the shared read pipeline no longer keep separate copies of the passes a stored document goes through before it is read. Nothing changes for a reader; the two could previously drift, and a reader that skipped the gating pass would publish content the page deliberately withheld.
- #651 `f054383` Thanks @mobeenabdullah! -
@nextlyhq/blocks-reactnow exports the types its public API is written in.
StyleCompileContext, BlockDocument and DocumentLimits appeared in the built
declarations in parameter positions while being named in no export statement,
and BreakpointSet — the one field StyleCompileContext requires — was absent
from the surface entirely. A host could see the name it was required to pass and
had no way to write it down, because those types originate in
@nextlyhq/blocks-engine, which is a dependency of this package rather than a
peer.
The root entry now re-exports the engine types the surface is built from, and
the set is CLOSED: an exported type is only as writable as its parts, so a host
handed BlockDefinition could name it and still not write down the supports
object it must pass or the seo() contribution it must return. Everything
reachable from a re-exported type is re-exported too, so annotating any part of
the surface needs no second package.
They live on the root entry rather than /next, whose declarations import the
next and nextly peers a standalone install does not have.
A regression test asserts each is named in an EXPORT STATEMENT of the built
.d.ts, not merely present in the file, and derives what is required from the
declarations themselves — the entries from package.json, the obligation from
the engine's own composition — so the check grows with the API rather than with
someone remembering to extend a list.
nextly's own route types are deliberately not re-exported: it is a peer
dependency, so a host names ContentEntry, RenderContext and the route shapes
from nextly/runtime where they live.
- #646 `743772f` Thanks @mobeenabdullah! - Add the @nextlyhq/builder package, which will hold the visual page-builder editor. It ships no features yet, so there is nothing to install it for: it exists now so the editor arrives under a name that is already reserved and already versioned in lockstep with the rest. It requires React 19, matching the renderer it draws with (@nextlyhq/blocks-react).
- #660 `ba3a72c` Thanks @mobeenabdullah! - Read and write hex colours from the server-safe colour entry point.
- #641 `a2f2080` Thanks @mobeenabdullah! - A content route no longer offers static generation it cannot perform.
createContentRoute and createBlocksPage read access-enforced content, so no
path they serve can be pre-rendered — and they now return no
generateStaticParams at all. Next classifies a route as static BECAUSE that
export exists, and every dynamic marking inside a static render is an error, so
an enforced route that also exported one answered 500 on every path whenever its
collection was empty at build time. Its runtime behaviour depended on whether
the database had rows in it when the build ran.
For public content that should be cached and pre-rendered, call the new
createPublicContentRoute / createPublicBlocksPage. They read trusted and do
return generateStaticParams.
Replaces the overrideAccess option on ContentRouteConfig, which had no
consumers: the posture is now stated by which factory you call.
- #657 `5d6f049` Thanks @mobeenabdullah! - Refresh five transitive dependencies to their patched releases, clearing the six open Dependabot advisories on this repository.
brace-expansion to 5.0.9 (denial of service through unbounded intermediate arrays, bypassing the earlier mitigation), fast-uri to 3.1.5 (host confusion via a backslash authority introducer), js-yaml to 4.3.1 (quadratic CPU consumption resolving !!omap), undici to 7.29.0 (five advisories, the highest being cross-user information disclosure and a parse-time crash on degenerate private cache directives) and dompurify to 3.4.13.
The DOMPurify advisory is the one worth an explicit reachability answer, because two published packages sanitize with it. Reaching it needs IN_PLACE sanitization together with a hook that removes a containing element, and neither sanitizer is that shape: sanitize-svg hooks uponSanitizeAttribute, the embed sanitizer hooks afterSanitizeAttributes, both are attribute-level, and neither sets IN_PLACE. So the bump keeps a dependency on a supported release rather than closing a live hole. Both sanitizer suites pass on 3.4.13.
Each override floor is raised rather than left to resolve upward on its own, because all five were pinned in the lockfile at exactly the last vulnerable patch, and a floor that still admits a vulnerable version lets the next lockfile refresh land back on one.
These are pnpm overrides, so they govern this workspace's builds, CI and local development and do not travel with the published packages. What a consumer of nextly or @nextlyhq/plugin-page-builder resolves for these transitive dependencies is still decided by their own tree.
- #658 `d23b9d7` Thanks @mobeenabdullah! - Report conflicting shortcut-provider options when neither provider attaches a listener.
- #670 `3b88fff` Thanks @mobeenabdullah! - A scoped API key is now judged on its own grants for every Direct API collection and single operation, not just some of them. Previously a key holding only update access could read through operations that forwarded the caller identity without the key scope, because the service fell back to the permissions of the user who issued the key.
- #661 `edf2b04` Thanks @mobeenabdullah! - Stop publishing the rules of a block that draws nothing.
A block can declare that its props make it draw nothing, and core/image with no source and core/embed with no src both do. The stylesheet did not consult that declaration, so every rule compiled for the markup such a node WOULD have drawn was still published — matching no element, and naming whatever it referenced. An image block waiting for its picture announced the URL of a background it never painted.
The declaration now reaches the style compiler, which holds those rules per node rather than emitting them into the main sheet, exactly as it already does for a condition-gated node. A page compiled since carries an entry for each drawless node, and the reader appends only the ones that draw.
What made this worth doing carefully is the direction it must NOT go. Dropping a node from the style input marks the document repaired, and a repaired document with nothing to recompile from has its whole stylesheet withheld. Blanking every rule on a page because one image is waiting for its picture is a far larger regression than the unused bytes it saves, and an unfilled image is an ordinary authoring state rather than the exceptional one the other prune cases describe. So a stored sheet that predates this keeps its node and ships whole; republishing the page compiles the entries and the drop starts working, with nothing to invalidate by hand.
declaresNoMarkup in @nextlyhq/blocks-engine is now the single implementation of the question. SEO derivation had its own copy and now shares this one, so the compiler, the renderer and the derived metadata cannot answer differently about the same node. It fails in the opposite direction to isConditionGated, and deliberately: an unreadable visibility condition must count as gated or hidden content leaks, while a block that throws or answers with a non-boolean must count as drawing or a node that is on the page loses everything derived about it.
Block-type default rules stay in the main sheet, because they come from the block package rather than from the document and a sibling of the same type that does draw still needs them.
- #645 `249649e` Thanks @mobeenabdullah! - nextly: record what email was sent, and what failed
A failed password-reset previously left no durable trace — the adapter threw,
the service returned { success: false }, one line went to the process log,
and the operator learned from the user. Sends are now recorded in
email_deliveries.
The table stores a hash of the recipient rather than the address, and a template slug rather than a rendered subject, so it answers "did this send" and "how many failed" without answering "to whom". Provider failure messages have address-shaped text removed before storage, because an SMTP rejection quotes the recipient back at you.
This is a log, not a queue: nothing drains it, and the retry columns it carries are reserved and inert so that adding a drain later is not a migration on a table already holding history.
The recipient column is a KEYED hash rather than a bare digest. An email address carries too little entropy for a plain SHA-256 to resist an offline dictionary, so anyone holding the table could confirm whether a given person was written to. Keying it with the install secret leaves the support lookup working unchanged while making the column unreadable without that secret. The schema no longer claims the table sits outside identity-erasure obligations, because a keyed hash of an address is pseudonymised data rather than anonymised data.
A send whose bookkeeping fails after the provider accepted the message is no longer reported as a provider failure. Acceptance is recorded the instant the provider answers, so deriving the response cannot turn a delivered message into a full set of failed rows, an after-send action told the send failed, and an auth flow withholding a token.
Provider containment now covers the stages that run with parsed configuration: building an adapter and probing a connection. A parser that derives a credential left both quoting the derived value into a diagnostic that reached the failure log, because the needles were computed from the stored form alone. A parser that renames one is refused outright, for the same reason a parser that shortens one already was.
The provider's own verdict survives a failure in the bookkeeping that follows it. Recording only that the provider answered, and defaulting to success, turned a refusal into a delivery and had an auth flow withhold its undelivered-token fallback for a message that was never sent.
The notice written when a row is kept without its provider reference can no longer change what happened. An installed logger that threw was caught by the recovery's own handler and reported as a retry that failed, for a row sitting in the table.
- #694 `e0e7714` Thanks @mobeenabdullah! - fix(nextly): take the HTTP status from the error code, and record template changes
Eight throw sites restated a status the canonical map already answers, so the number lived in two places and only one would be found by someone changing it. The status now comes from the code alone.
Deleting an email provider nulls the reference on its delivery rows rather than removing them, so the log stays evidence of what was sent. That behaviour now has per-dialect coverage on PostgreSQL and SQLite, where it was previously untested. MySQL still has no such constraint: adding one requires nulling pre-existing dangling references first, which nothing in the schema pipeline does yet.
Email template mutations now reach the activity log. A template decides what a password-reset message says and who it appears to come from, and that change was previously invisible after the fact. Entries carry field NAMES only.
- #626 `fe694de` Thanks @mobeenabdullah! - Email providers are now described by a definition, so a plugin can add one that works everywhere a built-in does.
A contributed provider could previously be registered but never configured: the REST API and the provider service both validated the type against a fixed list of the three built-ins, and defineConfig resolved providers through a hardcoded switch. Registration is now the only thing that decides which types exist.
A provider definition also declares its configuration fields, which values are secret, and how to validate them. Secrets are redacted because the provider says so rather than because a key name looked sensitive, and an invalid configuration is rejected when it is saved instead of when a send later fails.
- #690 `968b7ce` Thanks @mobeenabdullah! - fix(admin): replace one part of the email provider form without resetting the rest
Changing a provider type, or a plugin returning while the form is open, replaced the configuration through a whole-form reset. That makes every current value the form's new baseline, so fields it never meant to touch stop differing from it — and reconciling a refetch keeps only what still differs. A rename typed before either of those happened was silently overwritten by the record's own value.
Each of those now writes only the fields it means to, and a provider type chosen in the picker is kept as the operator's until they save. A descriptor that gains a configuration field while a form is open now initialises it, so a switch no longer draws a position the form does not hold, and a field being edited is left alone.
- #638 `4b2c025` Thanks @mobeenabdullah! - Ask one host list, from both channels a page fetches through.
BlockHostPolicy now carries remotePatterns, in the same shape a Nextly app already declares in next.config for next/image, so copying the entry across just works. A block writes an <img src> or an <iframe src>; a compiled stylesheet writes url(...) into a rule that fires on every page it applies to. Both turn a stored value into a request, and both now ask THIS list rather than each keeping its own, because a policy two surfaces answer differently is not a policy. The style channel asks it through the predicate the engine takes, so the two cannot drift.
core/image and core/embed consult it. For the image, the check is applied to whichever URL was SELECTED rather than to the typed one alone: a URL the resolver returned came out of a media record a person filled in, so it names a host on the same terms the typed prop does, and checking one of the pair leaves the other unbounded.
core/embed consults it, and an unlisted host renders nothing at all rather than an empty frame, for the reason the empty source already renders nothing: a frame with no usable source loads the page inside itself in several browsers. A caller who passed their own mayFetchUrl keeps it, since that is the more specific answer and deriving one here would silently replace it. Absent means unasked rather than allowed-nothing, so a host that configures no list renders exactly as it did before.
Enforcement is per-renderer, and the type says so where someone reading it will find out. The boundary cannot apply this on a block's behalf: it sees the element a block RETURNED, not the URLs the block chose, and an <img src> deep inside returned markup is indistinguishable to it from any other prop. The blocks shipped here consult the list; a block written outside this package is bounded by it only if it asks. A site wanting a hard limit should pair this with a content security policy, which the browser enforces whatever a block does.
core/embed's rendersNothing still answers from its props alone, deliberately. The declaration is read without a render and so has no policy to consult; a URL the policy will refuse is reported there as output and then draws nothing. That direction costs an empty rule in a stylesheet, where the other would claim a drawing block draws nothing.
A stored stylesheet now records which policy compiled it. The artifact is a CACHE of a compile, and a cache is sound only when it is keyed on every input that compile used; the fetch list is such an input, because the same document compiled under two different lists produces two different sheets, one of which may name a host the other refuses. Without that key a sheet written before a policy existed keeps publishing url(https://unlisted…) on a site that has since forbidden it, with the block markup beside it bounded and the stylesheet not.
So PageStyles gains an opaque fetchPolicyId, derived from the patterns themselves rather than assigned, so it changes exactly when they do and there is nothing to remember to invalidate. A reader whose policy does not match the stamp treats the sheet the way it already treats one compiled from a larger tree: recompile when the inputs are there, withhold the CSS when they are not. A sheet that WAS compiled under the current policy is still served from the store, which is why this is a stamp rather than recompiling unconditionally: a site with a policy does not pay a compile per render.
fetchPolicyLabel is public because the write path needs it. A writer that could not compute the same label would stamp nothing, every stored sheet would read as stale, and a site with a policy would recompile for ever.
The type documentation no longer claims every field defaults closed, because two fields now default differently and a host reading the old sentence could omit configuration believing remote fetches were denied. trustedFrameOrigins defaults closed, since the grant it controls lets a frame script the page around it. remotePatterns defaults OPEN, because it arrived after the renderer shipped and defaulting it closed would stop every existing site loading its own images the day it upgraded.
core/image asks the list BEFORE choosing between its two candidates rather than after. Selecting first and filtering after meant a library image the site will not fetch beat a perfectly good typed URL and then took the whole block down with it: the author was left with nothing because of a setting they cannot see, while the fallback they wrote sat unused. Filtering first makes the block render the first candidate it is actually allowed to load, which is what a fallback is for — and it is what the link-preview path does with the same pair, so the page and the preview can no longer choose different images. A record whose URL is refused is dropped WHOLE, since its alt text and intrinsic size describe the asset that was refused.
The page-builder's own guidance is corrected in the same change. It told an integrator that @nextlyhq/blocks-react had no way to bound fetched hosts and to configure the separate page-builder renderer instead. That is now false, and believing it would leave the published page unbounded while the editor was configured — the editor refusing a host the live page then loads.
- #648 `1ddda0f` Thanks @mobeenabdullah! - Page editor: a page holding blocks under a slot that no longer exists now says so and offers to clear them. Such blocks are invisible on the canvas (a block only draws the slots it declares), so until now the page simply refused to save with nothing to select and nothing to delete. A bar above the editor names each affected block and where it sits, and removing one is a per-block choice that undo can reverse. Nothing is discarded automatically.
- #652 `38135e8` Thanks @mobeenabdullah! - Render a very long list instead of losing the block that holds it.
core/list mapped its stored items with no cap. A document's own limits bound node count and depth but never the length of a prop array, so items arrives at whatever length was written — and past the renderer's inspection budget the normalizer refuses the whole output. An accidentally long list therefore cost the reader EVERY item and left a broken-block marker where the list should be, rather than costing only the items past the end.
The items are clamped, and sliced before they are mapped so an oversized array is never walked in full: the work this bounds is the work of reading it, not only of rendering it. The cap sits far above any list a person writes and far below the budget, so nothing hand-authored reaches it and the block still has room for its wrapper.
- #634 `6823b57` Thanks @mobeenabdullah! - Adopt a neutral admin theme. The admin palette is now achromatic in both modes, with every asserted contrast pairing clearing WCAG AA by a margin rather than sitting on the gate. Control boundaries (text inputs, selects, checkboxes, the table search field) move to a visible 3.4:1 edge, active sidebar rows are filled with the surface their ink is declared against, and the dark table header surface no longer carries a hue the rest of the palette dropped.
- #663 `8b136ed` Thanks @mobeenabdullah! - The page builder no longer renders a second
mainelement. A page has one primary landmark, and the editor was adding another inside the admin’s own, which is invalid markup and gives screen readers two competing landmarks to choose between. The canvas pane is now a labelled region, so it is still announced and still reachable by landmark navigation.
- #686 `68145f1` Thanks @mobeenabdullah! - The page builder now names itself in the admin. Its entry in the plugins list and on the dashboard showed the raw package specifier where other plugins show a readable name.
- #600 `80723ec` Thanks @mobeenabdullah! - A preview link that names one entry no longer widens access to the rest of its collection: when the granted entry does not live at the requested path, the published-only fall-through now reads with the caller's own access instead of the trust the draft decision forced on.
- #601 `264bda2` Thanks @mobeenabdullah! - Minting a preview link now authorizes the entry it names, not just the collection: a caller bounded by a row-level rule can no longer mint a working link for a document they cannot read themselves.
- #609 `db83c18` Thanks @mobeenabdullah! - Let a block render nothing without being reported as broken, and test the core primitives through the boundary that wraps them.
A block that deliberately renders nothing, such as an image with no usable source, was replaced by a broken-block diagnostic when its node also carried an anchor id. Rendering nothing is a decision rather than a failure, and the two now have different answers.
Emptiness is judged only from what this renderer can vouch for, which is the part worth reading twice. Two things earn the exemption: the block DECLARES that its props draw nothing, through the rendersNothing contract, which is computed from data this renderer already holds; or the output is a value this renderer OWNS — a primitive React draws as nothing, or an array normalizeRenderable materialised, walked by index exactly as React walks it.
Nothing else. A wrapper the block returned is never opened to see whether it is empty. Its children, a provider's value, an element's key and ref, a Set's iterator and an array's iterator are all author-controlled, and React reads every one of them AGAIN after this check has returned — so an exemption granted on a reading React need not repeat is an exemption that can be wrong. It was wrong in five separate ways, two of which took the whole page rather than one block: an iterable that answered differently on each call, a Set carrying its own iterator, a getter hidden from enumeration, an inherited getter, and a stateful children accessor. The list of properties to probe was never going to close, because every one of them belongs to the author.
The cost is stated plainly: a block returning an empty fragment, an empty Suspense, a hidden Activity or an empty context provider, on a node that also asks for an anchor id, keeps its diagnostic. That block says rendersNothing if it means it, and then the exemption is granted from data rather than from a structure that can change underfoot.
The contract still covers every value React draws as nothing rather than the nullish pair alone. A plugin block written in the ordinary conditional form render: () => enabled && <element /> returns false when disabled, an empty string arrives from a cleared value, and a map over an empty collection arrives as []. A returned Set is materialised before it is read, so it counts too. 0 is deliberately excluded, since React renders it as the character zero: real output with no element to carry the node's fields.
A candidate URL clears BOTH filters before core/image chooses between them, and a media record whose URL either filter refuses is dropped whole. The two refuse different things — the scheme guard refuses a value that could execute, the host list refuses one the site will not fetch from — and this block had been caught twice applying one of them at one position of the resolver/typed-prop pair and not the other. The same pair reaches the link preview, so both run there too, and the preview publishes the URL in the form the guard normalised rather than the form it was handed.
SuspenseList joins the wrapper set the normalizer already accepted as renderable. A type accepted in one list and missing from the other is a wrapper walked to validate its children in one place and reported as output in the other.
The primitives were only ever tested by calling their render functions directly, which is not the path a page takes: the boundary appends the block type class, clones the node fields onto the root, and normalizes the output first. That gap is why this defect and two others reached main.
- #650 `0585842` Thanks @mobeenabdullah! - A public content route no longer expands relations by default.
A trusted read propagates both its trust and a widened lifecycle into
relationship expansion: a populated target is read with access rules bypassed
AND status: "all". At the inherited default of depth: 1, a page in a public
collection could therefore embed a draft or access-restricted row from a
collection appearing nowhere in the route config — and a public route
pre-renders that into a static artifact.
createPublicContentRoute and createPublicBlocksPage now default to
depth: 0. Setting depth explicitly restores expansion, and states that the
populated collections are public too.
- #654 `a3e1849` Thanks @mobeenabdullah! - Re-decide a held shortcut key on every repeat, so a binding whose action changes its own condition stops permitting the browser default.
- #678 `ed5e26e` Thanks @mobeenabdullah! - stop the sidebar content panel from emitting a second main landmark
- #685 `038935d` Thanks @mobeenabdullah! - A Single's schema change now applies its table change and writes its registry row in one place, and the row records the outcome the apply actually reached. Saving a Single that only toggles Internationalization or Draft/Published now records that its companion table was provisioned, and re-saving a Single whose table failed to create can rebuild it and report success instead of staying stuck on "failed" however many times it is retried.
- #635 `9c12a68` Thanks @mobeenabdullah! - Let a site say which hosts its stylesheets may fetch from.
A stylesheet is a fetching surface. background-image: url(...) makes the browser request whatever it names, on every page the rule applies to, and until now the only limit on that was the scheme allowlist. That allowlist answers whether a URL is http(s) rather than javascript:; it has never had anything to say about WHICH host is reached. A value carrying no scheme at all can still name one, because //cdn.example/a.png inherits the page's protocol and nothing else, so a check reading "no scheme, therefore this origin" was wrong about exactly the case that reaches somewhere else. The comment saying so has been corrected, and it is no longer the only thing marking the gap.
StyleCompileContext now takes a mayFetchUrl predicate, forwarded to every URL a compile can emit. A PREDICATE rather than a list of patterns, so the engine holds no matching rules of its own and the caller keeps ONE answer for every channel it owns; which hosts a site trusts belongs to the site, not to the document format. Left undefined, nothing is asked and a compile behaves exactly as it did before, which is what every caller outside a configured site gets. The question is put last, to a value already known to be well formed, so a host rule is never the reason given for a value that was going to be refused anyway.
Coverage is proved rather than asserted. The test walks the catalog for every leaf that can carry a URL, places a refused host at each one and checks none reach the stylesheet, with an allowed host in the SAME position as the control — without it a compiler emitting nothing for that property would pass by writing no CSS at all. Deriving the positions from the catalog is the point: a written list is a snapshot, and the property added next month would not be in it while the suite still reported full coverage.
Two signatures grew a parameter and are now grouped rather than lengthened. validateStyleValues already took six positional arguments and envelopeRules ten, which is past where a call reads by position; a further optional would have sat beside one of a different type with nothing but that type to tell them apart, and a policy lost in a mis-slotted call leaves every URL in the document unasked about. envelopeRules takes a named object instead, so its arity goes down rather than up.
- #693 `c4de051` Thanks @mobeenabdullah! - Serving a page through the new
preparePageForReadno longer publishes stylesheet rules for a block that is missing from the site, so an uninstalled plugin stops leaving its block defaults and named classes behind in the page CSS.
- #612 `3278f13` Thanks @mobeenabdullah! - Add a keyboard shortcut manager to the UI kit: one listener, with precedence that follows the component tree.
Shortcuts registered per component could not decide who owned a key. stopPropagation does not stop other listeners on the same node, so every global handler ran and the winner was whichever component mounted first. Pressing Escape during a drag could cancel the drag and navigate away from the page at the same time.
ShortcutProvider installs the single listener. A nested ShortcutScope outranks the shell around it, and a layer marked blocking also swallows the keys it does not bind, so a drag or a modal can hold the keyboard for as long as it is up. mod resolves to Command on Apple platforms and Control elsewhere, sequences such as g d are supported, and modifier-carrying shortcuts still fire while the user is typing.
- #672 `bb4ebd0` Thanks @mobeenabdullah! - Schema Builder: a unique column that a database cannot index is no longer described two different ways. The rule deciding whether uniqueness is a named index or an inline constraint now lives in one place and is asked by the create path, the add-column path and the desired schema alike, so a reconcile no longer proposes a unique index the server refuses.
- #649 `532ed04` Thanks @mobeenabdullah! - A column declared unique now gets a named unique index instead of an unnamed constraint written into the table itself.
An unnamed constraint is one the database names for you, and on SQLite that name is internal and cannot be referred to. Nothing could describe it afterwards, so the schema Nextly compared against never matched the table, and the only way SQLite could reconcile the two was to rebuild the whole table. Nextly refuses a rebuild it did not ask for, so the entire change was refused with it, including the parts that were only adding things. It also made such a column impossible to remove.
- #637 `891ec3b` Thanks @mobeenabdullah! - A repaired legacy column is now checked for JSON contents before it is converted, and the repair refuses without changing anything when the check fails. A field originally declared as text carries the same legacy column shape as a repeater, so the repair could be offered for prose — failing mid-migration on PostgreSQL, and on MySQL leaving the column renamed but unconverted because MySQL commits schema changes as it makes them.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/blocks-react@nextlyhq/builder@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 18 packages at 0.0.2-alpha.54 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #581 `8e75d40` Thanks @mobeenabdullah! - Typecheck the block renderer’s own tests, and give block authors a typed defineBlock.
The package excluded test files from tsc, so its tests had never been typechecked. Adding a tests project surfaced eleven errors, nine of which shared one cause: the engine types a slot’s output as unknown because it carries no React types, so a block author could not place it in their own JSX without annotating every render by hand.
@nextlyhq/blocks-react now exports its own defineBlock, which names the context and the slot return type. This is the same service the plugin SDK performs for plugin authors, offered to anyone rendering with this package directly.
- #586 `8e81c4f` Thanks @mobeenabdullah! - Field access rules can ask what the caller is granted, and custom CSS is now a privilege.
A field's access.create / access.read / access.update function now receives
permissions and roles alongside req, so a field can be gated on a permission
rather than only on a role. Collection-level access already received these; field
level did not, so "only these people may write this field" was not expressible.
The grants are resolved once per operation and only when a rule actually runs, so
an entity with no field rules makes no extra lookup. A rule that cannot read the
grants denies rather than opens.
permissions uses the same resource:action spelling collection-level access
uses. Note this differs from the action-resource form the database and the
admin's permission matrix show for the same row.
The page builder's per-page and per-block custom CSS now requires a new
write-builder-custom-css permission. Without it the CSS already on a page stays
visible and keeps applying, but cannot be changed — the field is dropped from the
write rather than the write being rejected, so everything else on the page saves
normally. Grant it to any role that should keep authoring custom CSS.
- #578 `a363c67` Thanks @mobeenabdullah! - Add nine core block primitives.
Heading, text, list, quote, image, button, spacer, divider and embed join the containers already in the library, which is enough to build a real page. Each is a single element with no wrapper, no default padding and no hardcoded colour: styling belongs to the style system.
The accessibility contracts are part of the blocks rather than left to the author. A heading renders the level the author chose rather than one derived from nesting, so the page outline does not change when a block moves. A button renders an anchor when it has a destination and a button when it does not. An image always emits alt text, empty when it is decorative. A quote keeps its attribution outside the quotation. An embed is sandboxed, carries a title, and does not leak the page path to the embedded party.
- #585 `c2ca409` Thanks @mobeenabdullah! - Let each block claim its own DOM id at render, instead of reserving ids in advance.
Which node ends up writing an id is only knowable once a block has run: one that throws, or returns something with no host root, is replaced by a placeholder that emits no id at all. Reserving ids before rendering therefore meant a block that later failed had already taken the id, and the healthy node that wanted it rendered without one in exchange for nothing.
Node ids are still made unique before rendering. Those are React keys, and a duplicate makes React reuse one block’s instance for another, which is a wrong page rather than a missing anchor.
- #394 `2892263` Thanks @faisal-rx! - Fix localized entities breaking schema applies and singles reads: SQLite/MySQL schema syncs no longer fail once a
_localestable exists, singles created in another dev worker resolve without a restart, enabling Internationalization without alocalizationconfig is rejected with a clear error (and the builder switch explains it), and adding thelocalizationblock to nextly.config now takes effect without a manual restart in dev.
Collection and single tables created on SQLite or MySQL from now on also get the indexes Postgres and the Schema Builder already created for them, including the unique index on slug. Creating an entry with an explicit slug that another entry already uses now fails with a duplicate error on those dialects instead of being accepted silently. Tables created before this release keep the shape they were created with and are not backfilled, so an existing collection continues to allow duplicate slugs until its table is rebuilt.
- #595 `8b7ce78` Thanks @mobeenabdullah! - Report the class library slot that was dropped when the same class is listed twice. Only the first of two entries claiming one id or one name is written, and the warning explaining that named the entry rather than the slot — so a library built by reference, with one object in two slots, reported nothing at all and left an editor with no position to repair.
- #584 `f7229c8` Thanks @mobeenabdullah! - Refuse a plugin permission that collides with one a collection or single already owns, including for Schema Builder entities the config cannot see and for declarations that differ only in letter case. Honouring such a declaration hands the plugin a permission the role presets grant to editors, so the collection quietly stops being editable by them. An application already running such a plugin can set NEXTLY_ALLOW_PLUGIN_PERMISSION_OVERRIDE=1 to keep booting with a warning while it is fixed.
- #576 `8ff9c59` Thanks @mobeenabdullah! - Resolve a scoped preview link by the entry it names.
A preview grant that names an entry is now read by that id and confirmed to live at the requested path, instead of resolving the path by slug and comparing ids afterwards. A slug is not unique, so the old order could find a different document, reject it, and fall back to published, showing an editor live content at a link they were given for a draft.
When the named entry is gone or lives at another path, the request holds no draft authorization for that path and resolves published-only, so the widened lifecycle scope cannot surface a row the grant never named.
- #583 `e7e51d9` Thanks @mobeenabdullah! - Add the admin side of shareable preview links.
A previewLinkApi service and a usePreviewLink hook mint a link for one entry and put it on the clipboard. This is distinct from the Preview button beside it: Preview opens the entry using the editor’s own session and can include unsaved changes, while a preview LINK goes to someone with no session at all, so it carries its own signed authorization and shows only what was saved.
The link is minted per click rather than cached, because it carries an expiry and a cached value would be handed out after it stopped working. When the browser refuses clipboard access, which happens on an insecure origin, the link is shown rather than a copy being claimed that never happened.
- #580 `fdefbe2` Thanks @mobeenabdullah! - Add endpoints for minting and revoking preview links.
POST /api/nextly/preview-links mints a link scoped to one entry, gated on update for that collection rather than on publish: someone who can edit an entry already sees its draft, so sharing a link to it grants nothing new, while requiring publish would break the workflow where an editor who cannot publish shows a draft to a reviewer.
POST /api/nextly/preview-links/revoke invalidates every link ever issued, including sessions already in flight. It is gated on manage settings, because the generation it moves is site-wide.
The mint returns a token rather than a URL, since where the preview route is mounted is the application’s decision.
- #579 `5bf444e` Thanks @mobeenabdullah! - Stop shipping CSS compiled for blocks that render a placeholder.
A node that resolves to a placeholder emits only a hidden marker, so every rule compiled for the markup it would have rendered matches nothing and ships anyway, carrying whatever those rules referenced. The stylesheet is now compiled from a tree with those nodes removed, while the render keeps them so their placeholders still appear.
- #594 `5a0c8f6` Thanks @mobeenabdullah! - Add the stylesheet a whole site shares, compiled once from its design tokens, self-hosted fonts, named classes and block-type defaults, and named by a hash of the bytes it produced. Every page of a site repeats those rules today; a shared sheet is written once and cached until something in it actually changes. A token stored without any values is now reported and skipped rather than ending the compile, which would otherwise have taken down every page on the site.
- `a323af5` Thanks @mobeenabdullah! - Hold a conditionally-shown block's own styles out of the page stylesheet, returned separately so a reader can add back only the blocks it kept. A page's CSS is compiled when the document is saved and a condition is decided when the page is read, so one stylesheet otherwise carries rules — and any image URLs inside them — for blocks the reader removes. A page with no conditional blocks compiles exactly as before.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/blocks-react@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 18 packages at 0.0.2-alpha.52 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #532 `4902ef4` Thanks @mobeenabdullah! - Give a column added by an edit the constraints and indexes creating the table would have attached: a one-to-one is unique, a relationship is indexed, and a requested index exists. Adding a required relationship to a collection that already has entries is now refused with the steps that work instead of emitting invalid SQL, and removing a relationship drops its foreign key first on MySQL and is refused on SQLite, which cannot drop one without rebuilding the table.
- #526 `8bdf575` Thanks @mobeenabdullah! - Erase a deleted account's request identifiers from the auth log.
Deleting a user already removed their name and email from the activity log while keeping the record itself. The auth log identifies a person a second way — by the address they connected from and the client they used — and those survived untouched. They are now erased on the same deletion, stamped with when, while the event kind, the actor and target references and the timestamp stay: that is the security fact a retained trail exists for.
Erasure is keyed on the actor. A row naming someone as the TARGET carries the
address of whoever acted on them, so erasing by target would scrub a different
person's data and leave the subject's own in place. Events recorded without an
actor — a failed login, a rejected CSRF — are out of reach by design, since they
are written unattributed precisely so a failure cannot reveal which account was
reached; nothing links them to a person, so no deletion can find them. This
table is pruned on audit.retention.authMaxAgeMs — 180 days by default — so a
window is what bounds them. A window is a weaker guarantee than an erasure,
which is why the metadata projection below is default-deny: what never enters is
the only thing certain not to persist.
Whether each table can be erased is now decided per table. A database can carry one and not the other, and answering for the pair would let a missing auth log suppress the activity erasure, leaving behind the names and emails the deletion exists to remove.
Identifiers are also kept out of the auth log's metadata in the first place. A
NextlyError's logContext is written for operator triage, and a failed login
puts the attempted email address there; the auth handlers copied that context
into the stored event wholesale. A failure is recorded with no actor precisely
so it cannot reveal which account was reached, so nothing links such a row to a
person and the deletion that erases their other rows can never find it — the
identifier has to not be stored rather than be erased later. Only an allowlisted
set of diagnostic keys is now copied, default-deny, so a key added for logging
cannot silently become a field of the audit trail.
Naming a key is not enough on its own, because none of the values are ours to
begin with. An AuthStrategy is application code and chooses its own failure
reason; an error's code accepts any string, and the two diagnostic codes are
copied straight from it. Each retained value is now checked against a vocabulary
this package controls — a reason it produces, or a code the canonical table
defines — and anything else is dropped. The value still reaches the operator log;
what it no longer does is enter a trail nothing can associate with a subject.
The reasons are named in one place that the handlers emitting them now compile
against, so a new reason is a type error until it is listed rather than being
discarded without a diagnostic. Three that the initial-password exchange already
emitted were being discarded that way, leaving pending-token-wrong-challenge, a
stale must-change state, and a missing user indistinguishable from each other in
the trail. All three are recorded again.
Upgrading: rows written before this change are not covered. The handlers
previously stored the whole error context, so existing unattributed
login-failed rows can already hold an attempted email address or a user id.
Deletion is keyed on the actor and those rows have none, so nothing reaches them
— the projection applies only to failures recorded from now on.
Accounts deleted BEFORE this change are not covered either, for the opposite
reason: their attributed rows still hold the address and client they connected
from, and the erasure added here runs during a deletion — it can never run for
an account that is already gone. actor_user_id carries no foreign key, so
those rows survive as orphans pointing at nothing.
Scrub both once, before or after upgrading:
-- Rows recorded without an actor: the context the handlers used to store -- wholesale, which may name an attempted address. UPDATE audit_log SET metadata = NULL
-- Rows attributed to accounts that no longer exist: their request identifiers,
-- which the deletion that removed them never erased.
UPDATE audit_log SET ip_address = NULL, user_agent = NULL
WHERE actor_user_id IS NOT NULL
AND actor_user_id NOT IN (SELECT id FROM users);
`
The first discards the diagnostic codes on those rows along with the
identifiers. The second leaves actor_user_id in place — the trail should still
say that the same someone did these things, only not who they were. The event,
its outcome and its timestamp are columns, and neither statement touches them:
that is the security fact the trail exists for.
Upgrading, PostgreSQL and MySQL: one required action. If you hardened
audit_log by revoking UPDATE — the posture this package previously documented —
grant it back for the three columns an erasure touches, or deleting a user will
fail and roll back:
GRANT UPDATE (ip_address, user_agent, identity_erased_at) ON audit_log TO app_role; GRANT DELETE ON audit_log TO app_role;
Two duties need those grants. Erasing the address and client a deleted account
connected from is an UPDATE, and it runs inside the deletion's transaction, so a
blanket revoke blocks account deletion outright. Pruning rows past their window
is a DELETE, and a role without it fails every pass silently — retention must
never fail the request that offered it — so the table grows unbounded while the
setting reads as enforced. Revoke DELETE only together with
audit: { retention: { authMaxAgeMs: false } }, so the configuration says what
the privileges actually do. Every other column stays immutable. Deployments that
never restricted these grants, and all SQLite deployments, need no action.
***
Prune the activity and auth trails on a schedule.
This deletes data the first time it runs. Set the windows before you deploy if you need longer ones.
Neither trail has ever actually been pruned. activity_log has claimed a 90-day
policy in its own schema comment since it was introduced, but the cleanup that
comment named was never called from anywhere — and could not have worked if it
had been, because it referenced a column that does not resolve and its failure
would have been swallowed. Installs are therefore carrying every row ever
written, while the schema said otherwise. audit_log never promised anything
and grew unbounded too.
Both are pruned now, and the first pass removes everything already past its
window:
- activity_log — content activity, who changed what — 90 days
- audit_log — sign-ins, password changes, role grants — 180 days
90 for content activity is what the comparable self-hosted CMSes default to, and 180 for auth events is what GitHub and Atlassian Cloud retain: security questions are asked later than editorial ones, because a compromise is usually noticed well after the sign-in that caused it.
To keep more, configure it before upgrading:
export default defineConfig({
audit: {
retention: {
activityMaxAgeMs: 365 * 24 * 60 * 60 * 1000,
authMaxAgeMs: false, // keep auth history forever
},
},
});Each window is independent, so bounding the high-volume feed while keeping
security history indefinitely is one setting rather than a compromise.
audit: { retention: false } keeps everything, as today.
Passes run opportunistically off content writes, at most one per interval,
batched, and never fail the write that offered them. Batching matters on the
first run in particular: an install that has never pruned faces every row it has
ever written, and an unbounded DELETE there would take a long lock on the
largest table at the worst possible moment.
Scheduling is now shared rather than duplicated. The gate, interval and never-throw wrapper that webhook retention already used are a general mechanism, so audit retention registers a pass with it instead of introducing a second one. Each pass is gated on its own key: a single shared marker would let whichever pass ran first consume the interval for the others, and the busier domain would starve the rest indefinitely.
- #539 `49d44ae` Thanks @mobeenabdullah! - feat(blocks-react): add the React renderer package boundary
Adds @nextlyhq/blocks-react, the React/RSC renderer for Nextly block
documents. This change lands the package and its layering guarantees; the
renderer itself follows.
The root entry imports no next/*, no admin code and no CMS runtime, so a
document can be rendered from a plain React app, a test or a script. Everything
Next-coupled lives at the @nextlyhq/blocks-react/next subpath, so importing
the renderer never pulls Next into a consumer's module graph. Both rules are
enforced by an allowlist-based import test rather than by convention.
PageContext and BlocksDataProvider are also introduced: the seam through
which data, media URLs and entry paths reach a block, so blocks never reach for
a database directly.
- #536 `d53bc9f` Thanks @mobeenabdullah! - A text column keeps the width the builder that created it gave it.
A text field that states no width does not have one right answer. Three builders create tables and
they read a width from different keys and read silence differently: the Schema Builder's collection
creator bounds on a short variant, its field-group creator bounds on a declared maxLength and
never looks at a variant, and code-first tables were built with a bounded default. Which rule
applies is a fact about the entity, not about the field.
Describing a column without that fact meant guessing, and each place that guessed got it wrong for at least one builder. On MySQL a field group's short text field was described as unbounded when it had been created bounded, so a schema preview reported a type change on a column nobody had touched, and applying it would have rewritten the column. The same guess reached the localization companion tables, Single identity seeding, and the path that adds a column to a table that already exists.
The builder is now named wherever a column shape becomes DDL, so the width follows the table rather than being re-derived from the field. Paths that only look a table up to run a query are unaffected: a declared width is enforced by the database, not by the ORM.
- #514 `bffeac4` Thanks @mobeenabdullah! - Custom CSS in the page builder can no longer load anything from another origin.
- A
url()carrying a scheme or a host is refused, and the editor says which - declaration went and why, with a remedy that works whichever storage adapter the
- media library uses.
This closes a way of reading data off the page. A selector that matches only on
a prefix, paired with a URL that fires a request when it matches, spells a value
out one character at a time — input[value^="a"] { background: url(...) },
repeated. Custom CSS is the only surface where an author writes both halves, so that is
where the ban is absolute.
Banning it in custom CSS alone would not have closed the channel, because the two halves need not be written in the same place. A block's background image is compiled into the same stylesheet, so a remote image there plus a custom selector that suppresses it conditionally still leaks by the request's ABSENCE, with no URL in the custom CSS to refuse.
So a block's images are restricted the same way, and a site declares the hosts
it loads from. A relative path such as /media/a.png needs nothing; anything
carrying a host needs an entry, INCLUDING an absolute URL on your own domain,
exactly as next/image already requires:
<PageRenderer
document={doc}
remotePatterns={[
{ protocol: "https", hostname: "cdn.example.com", pathname: "/img/**" },
]}
/>The policy covers every value a block emits, not the properties someone
remembered can fetch: filter: url(…) is a request too, and so is
filter: var(--missing, url(…)), whose URL lives in a fallback the parser
leaves as raw text. A protocol-relative //host/a.png is refused rather than
resolved against a guess, since the document's protocol is not knowable when the
stylesheet is compiled.
BREAKING, and wider than images: every resource a block loads on its own is now
refused until its host is declared. On upgrade, add the hosts below to
remotePatterns or the content stops rendering.
| block | what stops | host to declare |
| --------------------------------------------- | --------------------- | --------------------------------------- |
| core/image | the image | wherever your media is served from |
| core/cover, core/slides, flip cards | the background | same |
| core/gallery, the carousels, core/hotspot | the images | same |
| core/video | the source and poster | your media host |
| core/lottie | the animation | the animation's CDN |
| core/embed (URL mode) | the iframe | e.g. www.youtube.com |
| core/map | the iframe | www.google.com, or your own tile host |
This includes absolute URLs pointing at your own site: nothing in the compiler
knows what your host is, so https://your-site.com/a.png needs an entry while
/a.png needs none — the same line next/image draws. If your media library
stores absolute URLs, which the cloud storage adapters do, declare your own host.
A custom block registered from outside this package applies the policy itself:
its render receives remotePatterns, and mediaUrl / cssMediaUrl are
exported for it. The renderer cannot inspect the element a block returns, so a
block that writes a URL into an src or an inline background without asking
reaches whatever host it names. The shape is Next.js's images.remotePatterns, so an entry can
be copied straight across from next.config, and the posture matches
next/image — nothing off-origin unless you said so. Matching uses picomatch
with the same options next/image uses, rather than an approximation of it, so
hostname and pathname globs mean exactly what they already mean in your
next.config. search is honoured too.
Everything the sanitizer removes is now reported rather than dropped silently, including at-rules it does not support. A rule that disappears with nothing on screen to explain it reads as a bug in the builder, and the author's own source still contains the line that did not survive.
CSS the sanitizer cannot read through — a rule nested deeper than it follows, or a fragment it cannot parse — is still removed, but it is now reported as unchecked rather than as a remote URL. It previously named the whole rule as the offending address, which sent authors looking for a host their stylesheet never mentioned. The depth it follows also rose well past real CSS: the old limit refused valid stylesheets at five levels of nesting, which ordinary compiled CSS reaches.
BREAKING, for anyone calling the sanitizer directly: sanitizeCustomCss and
sanitizeBlockCss return { css, warnings } rather than a string. They are
re-exported from the package root, so this is a visible change even though the
page builder itself is the only expected caller. Read .css where you read the
result before.
Also on that surface: CssWarning["code"] gains "unchecked", which a switch
over the union has to handle, and CSS that fails to parse outright now reports
"unchecked" where it reported "unsafe-value". MAX_RULE_NESTING and
MAX_VALUE_NESTING are exported alongside them.
- #528 `938898d` Thanks @mobeenabdullah! -
create-nextly-apprecognises the development-diagnostics setting however an existing.env - spells it, and no longer mistakes a different variable for it.
A substring test treated NEXTLY_DEV_DIAGNOSTICS_BACKUP=1 as the setting already being present,
so such a project was skipped and never told the real one exists. The check now matches an
assignment at the start of a line, including the commented form and the export KEY=value form
dotenv accepts so a file can also be sourced by a shell.
The whitespace in that match is confined to the current line. Allowing it to cross newlines made
the scan backtrack across the blank lines an .env is full of, which is quadratic on the common
case of a file that does not contain the key at all.
- #537 `a281098` Thanks @mobeenabdullah! - The Direct API types a row the way the process sees it: a timestamp is the Date the driver decoded, not the formatted string a REST response carries. Codegen records which fields a collection or single stores in a timestamp column, and the wire types are unchanged.
A write returned an undecoded row on the raw-SQL paths, so a created row carried epoch numbers on SQLite where a fetched one carried Dates. Every raw-SQL row now decodes the way a read does.
The media services name the error code they mean rather than leaving the boundary to infer one from a status, so a folder-name clash keeps saying "already exists" instead of "reload".
- #529 `17be415` Thanks @mobeenabdullah! -
SubmissionDocument.statusnow includes"spam", and gainsspamReason.
The stored field has always offered spam, the admin has a Spam tab and filters its other views
with not_equals: "spam", the notification hook skips it, and marking something "Not spam" moves
it back to new. Only the TypeScript type disagreed, so it described a shape the database cannot
produce — narrowing on status could not see the case that actually reaches the UI.
The conversions from a stored row to this plugin's document types now live in one module rather than at six call sites. They are still unchecked assertions, which the module says plainly: the services layer answers with a loose row and TypeScript has no overlap to verify. Nothing about runtime behaviour changes; the unchecked step is now in one place a reviewer can find.
- #521 `d58130a` Thanks @mobeenabdullah! - Keep the Schema Builder's DDL generator, the column descriptor and the write path agreeing on which
- fields are junction-backed. A field carrying
relationType: "manyToMany"was treated as - junction-backed by the descriptor whatever its type, while the generator emitted a junction table
- only for a
relationship. Anuploaddeclared many-to-many therefore got a parent column that the - runtime schema and the schema diff did not know about, so the diff proposed dropping it on every
- apply.
Junction storage is a relationship feature, because that is the only shape the read and write
paths implement, so an upload carrying that option keeps its own column and is unaffected: a
single target is a foreign key, hasMany or an array of targets a JSON array of ids. A
relationship many-to-many is unchanged — no parent column, one junction table.
- #519 `3a1b43b` Thanks @mobeenabdullah! - One table now decides what an HTTP status means when a failure names no error code.
Three tables used to, and they disagreed. The same code-less 401 reached a Direct API caller as
AUTH_REQUIRED and a REST caller as INTERNAL_ERROR; a code-less 429 lost its rate-limit
identity entirely, and with it the Retry-After a client needs to back off correctly. The media
service kept a third table that read 409 as DUPLICATE and 422 as BUSINESS_RULE_VIOLATION.
A code-less failure now resolves through one shared table for 400, 401, 403, 404, 409, 413, 415, 422, 429, 502 and 503, and anything unrecognised stays an internal error. The producer's own status is preserved rather than rounded to the code's canonical one.
The table is a fallback, not a translation. A status is coarser than a code: 409 covers both
"that name is taken" and "someone else edited this", which need opposite advice. A service that
knows which one it means sets code and is believed. MediaResponse, DeleteMediaResponse,
FolderContentsResponse and the folder bulk-delete result can carry a code for exactly this
reason, and creating a folder whose name is taken now says so through DUPLICATE rather than
relying on a boundary to guess.
A code-less failure never puts its own message on the wire. Those envelopes come from legacy converters that may store a raw exception's text, so the caller gets the generic sentence for the derived code and the detail stays in the operator log. A failure that names a code keeps its own message, which the producer authored to be read.
Behaviour changes worth checking if you read error bodies directly: a code-less 401 answers
AUTH_REQUIRED instead of INTERNAL_ERROR; a code-less 429 answers RATE_LIMITED; a code-less
422 answers INVALID_INPUT; and through the Direct API a code-less failure's message is now the
generic sentence rather than the service's raw text.
- #538 `4f009ae` Thanks @mobeenabdullah! - A plugin can now hand its own configuration to its own admin components.
A plugin's factory runs on the server, where the host builds its config; its
admin components run in the browser. Nothing carried a value between the two, so
a plugin could ship behaviour it had no way to configure. contributes.admin.clientConfig
travels with the rest of the admin metadata, and usePluginClientConfig reads it
back. It is PUBLIC — /api/admin-meta needs no authentication, so it reaches
anonymous callers and must hold nothing secret — and the serializer refuses
anything that will not survive the trip rather than delivering a mangled copy.
The page builder uses it for remotePatterns. The editor canvas previously
enforced an empty allowlist while the published page enforced the host's, so it
hid images the live page shows.
Pass the SAME value to both pageBuilder({ remotePatterns }) and
PageRenderer. They are separate assignments: the plugin option configures the
editor, and PageRenderer reads only its own prop. Setting just one is what
produces a mismatch, in whichever direction you set it — a shared constant in
the host is the way to keep them equal.
- #523 `f835ca9` Thanks @mobeenabdullah! - New apps document the development error-diagnostics opt-in.
An error response is deliberately generic — a code, a public message and a request id — and withholds the log context and the underlying cause so a response cannot disclose driver output, table names or internal paths. That is right for a deployed app and unhelpful while building, where the withheld part is exactly what you need.
NEXTLY_DEV_DIAGNOSTICS=1 adds a _devDiagnostics field carrying that detail. It existed
already, and nothing mentioned it, so an author hitting an error had no reason to suspect a flag
would have named the cause. create-nextly-app now writes it into .env and .env.example
commented out, with an explanation, and docs/configuration/environment.mdx describes it with
a worked example.
It is documented rather than enabled: the flag is the second of two independent signals, and the
second exists because NODE_ENV is a runtime value a deployment can carry by mistake. A default
shipped in .env would be true in exactly that case — the one it guards against.
Installing into an existing project that already has a configured .env adds the note too, keyed
on its own absence rather than on DATABASE_URL.
- #541 `72c894b` Thanks @mobeenabdullah! - A timestamp is stored the same way whatever the server timezone is. The raw-SQL write paths bound a JS Date directly, so the driver serialized it with the local offset and a column declared without a time zone kept the local wall clock, while every read interpreted that wall clock as UTC. A row written and read back on a server five hours ahead of UTC came back five hours late. Values are now encoded through the column the same way a Drizzle query encodes them, on PostgreSQL and MySQL; SQLite was unaffected, storing unix seconds, which carry no zone.
Rows written before this on a server that was not on UTC keep the wall clock they were given, so a table can hold both conventions until those rows are corrected. Deployments running UTC, which includes every default container image, are unaffected either way.
- #543 `9ccff93` Thanks @mobeenabdullah! - Add two editor-shell primitives to the UI kit: a right-click context menu, and resizable panel regions whose split can be dragged or moved from the keyboard. Both are experimental until a first-party plugin uses them.
- #525 `6c77f8f` Thanks @mobeenabdullah! -
@nextlyhq/ui's release tags now reach the published types. Every export in the - barrel carried
@publicor@experimental, and none of it survived the build: - the declaration bundler flattens each re-export into one
export { … }clause - and drops the doc comment attached to the export statement, so an editor
- hovering
badgeVariantswas told nothing about its stability. The tags live on - the declarations now, where the bundler keeps them, and 229 of them reach
-
dist/index.d.tswhere there were none.
toast and ToasterProps are re-exported from sonner, so their declarations
are not ours to annotate; they stay tagged in the barrel only. cn and
uiPreset, which ship from their own subpaths, carry @experimental now as
STABILITY.md already classified them.
Twenty prop types were also promoted to @public, which is a widening rather
than a change of intent: STABILITY.md already guaranteed that a prop type
carries the same stability as its component, and every one of these belonged to
a public component while advertising @experimental — so the published type
withdrew what the component promised, and a plugin could not wrap Tabs or
Dialog without depending on something labelled unstable. The rule is now
enforced by a test rather than written down.
Modal scrims are a theme token. Six components wrote the backdrop inline as
bg-black/80, identical in light and dark and at four different strengths, so
it could be neither themed nor white-labelled and was invisible to every token
check the package has. --nx-overlay (with --nx-overlay-soft for a scrim over
content rather than the page, and --nx-overlay-strong for one that carries
text directly — a full-screen state screen, an image lightbox and its caption,
where the muted detail line rather than the heading decides the strength: over
a white page text-white/60 is 2.81:1 on the see-through scrim and 5.66:1 on
the strong one) is defined for both modes and used everywhere,
with bg-overlay / bg-overlay-soft utilities in the v4 theme AND in
@nextlyhq/ui/tailwind-preset, so the documented Tailwind v3 path generates
them too. Dialogs, sheets and the command palette now share one backdrop
strength rather than three.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/blocks-react@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.51 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #495 `90dbe11` Thanks @mobeenabdullah! - Deleting a user no longer deletes what they did. Activity-log entries carried a cascading
- foreign key to the account that produced them, so removing a user destroyed their entire audit
- trail. The entries now outlive the account, and the account holder name and email are erased from
- them at deletion time instead, leaving the record of what happened intact and attributed to an
- opaque id. The dashboard activity feed renders those entries as a deleted actor rather than a
- blank one.
- #520 `ab607c3` Thanks @mobeenabdullah! - The admin panel's stylesheet no longer publishes names into the page that hosts
- it. Its animation names and Tailwind's internal
--tw-*custom properties were - resolved for the whole document regardless of the scoping on its selectors, so
- a host defining
spin,fade-inor the same--tw-*registrations shared them - with the admin and the later stylesheet won. Both are namespaced now, and the
- build fails if either escapes again.
@nextlyhq/ui's Tailwind preset keeps its named-plus-default export shape,
which the build warns about. That shape is deliberate and now says so at the
build config as well as beside the code: a preset is consumed as a value, so
require() has to return it, and silencing the warning would change it back.
The field-UI kit gains ConditionRow (@experimental), exported from
@nextlyhq/plugin-sdk/admin alongside operatorsForType and
operatorTakesValue. It edits one condition as source / operator / value,
choosing the operators and the value editor from the source field's type, and a
source carrying an option list is compared against a dropdown of exactly those
rather than free text. It owns the row and not the container, so a surface keeps
its own chrome; pass operatorsFor to narrow the offered operators to the ones
your runtime can evaluate.
Both first-party condition editors now compose it. The schema builder's gains
nothing an author will notice beyond the value dropdown; the form builder's
gains type-aware comparisons, a dropdown for choice fields, and typed number and
date inputs. Stored shapes are unchanged in both, including the form builder's
comparison key and its seven-comparison vocabulary.
- #493 `d8d5bfe` Thanks @mobeenabdullah! - Keep the durable first-publication marker on every shape the entry editor uses, and let the
- editor trust it. A published entry that was unpublished and then reloaded no longer offers its
- slug back to the title generator, so republishing lands at the address the links already point
- at. The marker is consulted only for a slug shared by every language, because it records that a
- document was public somewhere rather than in one particular language.
The marker also survives editing: a document with a pending working draft now reports it on the save response and on the draft read, as a date rather than a string, matching an ordinary read.
- #515 `19efb3a` Thanks @mobeenabdullah! - The admin now reports a save whose follow-up actions failed, instead of showing it as a clean save.
A post-commit hook (afterCreate / afterUpdate / afterDelete) runs once the row is already
durable, so a handler failing there cannot un-save it. The server has always answered success and
carried the failure alongside as warnings, but the admin's entry clients returned only item and
discarded that array, so a search index that was not reindexed, a webhook that was not delivered or
a cache that was not purged looked identical to a clean write.
Creating, updating or deleting an entry now shows "Entry updated successfully, but 2 follow-up actions failed" with the failures behind a disclosure. It stays a success toast, never an error: the row IS saved, and reporting a failure would invite the editor to repeat a write that already took effect.
entryApi.create, entryApi.update and entryApi.delete now resolve to { item, warnings? }
rather than the entry alone. The onSuccess callbacks on useCreateEntry, useUpdateEntry and
useDeleteEntry still receive the entry, so callers of those hooks are unaffected.
- #504 `e7a675f` Thanks @mobeenabdullah! - Schema Builder tables keep the text column width they had. Creating a field group or single routed its columns through the shared descriptor, which read a text field with no stated width as bounded where the previous generator read it as unbounded, so on MySQL a new text column held 255 characters instead of 65 535.
A text field that limits its length now gets a column at exactly that limit, on every path that can build one: a field limited to 400 characters no longer lands in a column that rejects what its own validation accepts. The limit is the field's validation maximum, which is the one the Schema Builder has always sized a bounded column from. Localized companion migrations, Single identity seeding, and columns added to an existing table all recognise the bounded text column, so a freshly generated migration applies, a new Single keeps its seeded title and slug, and a column added at boot is not reported as changed on the next preview.
A field whose type belongs to a plugin that is not loaded also keeps the unbounded column it was built with, instead of being reported as a narrowing on a table nothing has touched.
A field group's text field that declares a maximum length keeps the bounded column it was created with. Its width is declared under a different key from a collection's, which the schema comparison did not read, so on PostgreSQL such a field was reported as a type change on a column that had not changed.
- #509 `c686245` Thanks @mobeenabdullah! - Behaviour change. A code-first collection or single that declares a field named
id, -
createdAt,created_at,updatedAtorupdated_atis now refused when the config is read, - instead of failing later during schema application. Any casing that resolves to one of those
- columns is refused too, so
CreatedAtis caught alongsidecreatedAt.
Such a collection could never have worked: the field is emitted alongside the injected column and the database rejects a table that declares the same column twice. The error now names the column it collides with, and arrives where the name is chosen.
title, slug and status are unaffected and remain declarable — the first two step aside for
an author's own field, and a status field is taken up by the draft/publish lifecycle.
- #507 `f348a0f` Thanks @mobeenabdullah! - Resolve a field name to its database column the same way everywhere. A Schema Builder collection
- created a field whose name began with a capital under an extra leading underscore, while the
- runtime schema and the schema diff addressed it without one — so the table and every read of it
- disagreed, and the diff reported the column missing on every apply.
Every decision about which column a field occupies now asks the same question of the same
conversion: which system column an author's field replaces, whether two names collide, which system
fields a config factory injects, and which columns an ALTER may touch. Two fields whose names reach
one column (such as foo_bar and FooBar) are now reported where the names are chosen rather than
failing during schema application, and editing a many-to-many field's index or flags no longer emits
statements against a column it never had.
Field types that store their values in their own tables, such as a component or a many-to-many relationship, are consistently treated as occupying no column: they neither collide with each other nor suppress a system column that still has to be injected beside them.
**Two configurations that were previously accepted are now refused at startup, with an error naming
the fix.** A field may replace the system title or slug column only under that column's own
name: title still works and is unchanged, while Title is refused, because it reaches the same
column while remaining a separate identity in every payload — a create carrying Title gained a
second generated title and the generated value overwrote the author's. And a field whose name
reaches a column the Draft/Published lifecycle owns is refused while that lifecycle is enabled; such
a collection could never have been created, since the column was declared twice. With the lifecycle
off, status remains an ordinary field name.
Emitted SQL is unchanged for every field name the Schema Builder accepts.
- #496 `387061e` Thanks @mobeenabdullah! - Reject a field named
id,createdAtorupdatedAtin a Field Group (component), through both - the visual builder and
defineFieldGroup. A component keeps its values in a table of its own - carrying those columns, so such a field is emitted into the same
CREATE TABLEas the injected one - and the database refuses the statement. The name is now refused where it is chosen, with a message
- saying which system column it collides with.
Field groups that already declare such a field could never have had a working table, since creating it fails; they will now be reported at configuration time instead of during schema application.
- #505 `e7316d8` Thanks @mobeenabdullah! - A core schema change now reaches a database that already holds content. Adding a column to
- one of Nextly own tables, or changing a constraint on one, was silently skipped on SQLite and
- MySQL whenever any content table existed, while nextly migrate still reported success. The
- reconcile now runs a second pass after a degraded one: with nothing left to create, the schema
- differ has no ambiguity to resolve and emits the alterations it previously abandoned.
- #510 `781fa81` Thanks @mobeenabdullah! - Custom CSS in the page builder can no longer end the
<style>element it is - rendered into. A value written with a CSS escape, such as
-
content: "\3c /style>", contains no markup as authored but was decoded into - markup when the stylesheet was serialized, and on a server-rendered page the
- browser then parsed whatever followed it as HTML. Those sequences are now
- escaped on the way out, so they still mean the same thing to CSS and nothing to
- the HTML parser.
Custom CSS also keeps its meaning inside :not(), :is(), :where() and
:has(). Scoping used to rewrite the selectors held by those, so
.a:has(> .b) silently became "has a .b anywhere under the page root".
- #508 `444bd26` Thanks @mobeenabdullah! - An error thrown by a Direct API call now chains the failure it actually came from. The public
- result shape drops the driver error and the identifiers the thrower attached, and the boundary
- rebuilt from what survived, so every unexpected failure arrived looking alike. The original is
- carried alongside the envelope and chained as the rebuilt error cause.
- #490 `a2e92ae` Thanks @mobeenabdullah! - Blocks now receive a render context, so a block that reads content is an
- ordinary async component rather than something the API had no way to express.
- A slot is now something a block draws rather than something it receives already
- drawn:
renderSlot(name, ctx?)replaces the map of rendered children, so a - repeater can draw its template once per entry with that entry's values, and a
- block that hides a panel no longer pays to render it.
A block's supports is checked against the catalog while it is being written
instead of at boot, and a plugin that registers its own support adds it to that
check by augmenting BlockSupportKeys in @nextlyhq/plugin-sdk/blocks. A key
lists the sub-flags it recognises as a union of strings, and declares either
never or true when it is all-or-nothing; both are read the same way, and a
sub-flag the key does not declare is refused where it is written. The
types a block definition asks for are all reachable from that same subpath, so
writing a block no longer means importing the engine directly. Renderers now
describe what they provide once by augmenting BlockRenderContext, so ctx is
typed without every block naming a context type of its own.
Breaking, in an experimental package:
- BlockSupportValue is no longer exported from @nextlyhq/plugin-sdk/blocks.
It is the shape the registry stores from every source, so as authoring
vocabulary it accepted a sub-flag name the per-key check refuses. Write a
shared setting for one key as BlockSupports["spacing"], or a whole object
through blockSupports().
- BlockRenderResult from @nextlyhq/plugin-sdk/blocks is now
ReactNode | Promise<ReactNode> rather than the engine's unknown, so a
helper typed with it satisfies a block's render.
- BlockRenderArgs.slots is replaced by BlockRenderArgs.renderSlot.
- BlockDefinition.resolve is removed. Nothing ever called it, so a data-loading
function written against it silently never ran; blocks read data through ctx.
- createRevision, pruneRevisions and Revision are removed from
@nextlyhq/plugin-page-builder. They duplicated the content-versioning
support that already ships in core, and nothing in the package used them.
- #512 `8c36bb6` Thanks @mobeenabdullah! - Record the outcome of every event the outbox captures. `success | failure |
- unknown` is the vocabulary the audit and observability schemas converge on, and
- the one field NIST SP 800-53 AU-3(e) requires that the envelope did not already
- carry.
Absence means success, which is what every event recorded so far is: a row is written inside the transaction of a change that commits, so a recorded event is by construction a completed one — and that is also why the column's default is the correct value for existing rows. The field exists so that a refusal, such as a denied publish, can be recorded as the distinct thing it is rather than being indistinguishable from a change that happened.
Additive and optional on the webhook envelope, so existing subscribers are unaffected.
- #513 `c9ef62a` Thanks @mobeenabdullah! - Record which retention window governs each captured event, and shorten the audit
- window to 90 days.
The event table has carried a retention_class column since the outbox shipped,
but nothing ever wrote anything but webhook, so every row was measured against
the short outbox-hygiene window. The class now follows from why the row was
recorded: a row admitted by the audit seam is audit-class and outlives outbox
hygiene, while one admitted only because an endpoint exists stays webhook-class.
A row that is both takes the longer window, since evicting it on the delivery
clock would lose history nothing can reconstruct.
The audit window default moves from 365 days to 90. The previous value was
justified as "SOC 2 practice is a one-year floor", which does not hold up:
neither SOC 2 nor ISO 27001 A.8.15 mandates a period — both require only that
retention be defined and risk-based — and the twelve-month figure is PCI DSS
convention that has spread into the wider discourse. 90 days is where comparable
products land for content activity. A deployment genuinely in PCI scope should
raise auditEventsMaxAgeMs, which is a decision only the operator can make.
auditEventsMaxAgeMs is now raised to eventsMaxAgeMs whenever the webhook
window is the longer of the two, including when it is false. A row admitted by
both the audit seam and an endpoint is labelled audit because that is the
longest retention it needs, so a shorter audit window would have pruned it
earlier than the webhook setting allows — irreversibly, and in a supported
configuration.
Upgrading, by deployment:
- webhooks.audit off (the default, and most installs): nothing changes.
Events are still recorded webhook-class and pruned on eventsMaxAgeMs exactly
as before.
- webhooks.audit on: events that used to be recorded webhook-class are now
audit-class, so they move from eventsMaxAgeMs to auditEventsMaxAgeMs — at
the defaults, from 30 days to 90. That is the intended behaviour, since those
rows are recorded for history rather than delivery, but it retains roughly
three times as many events and the storage that implies. Set
webhooks.retention.auditEventsMaxAgeMs if a shorter window is wanted.
- #489 `3a75d0e` Thanks @mobeenabdullah! - The admin now calls field groups "field groups" in the places that used to say "components".
The field picker, the Schema Builder's field-group editor, the entry form, the entries table badge, the Field Groups list and its empty states, and the dashboard's getting-started panel all carried the old wording, so a page titled "Field Groups" could tell you that you had selected components. Only the words changed: the stored field type, table names and API payloads are untouched, so no data or integration is affected.
- #465 `97bcb2c` Thanks @mobeenabdullah! - Collections and singles with Draft/Published now record when a document first went live, in a new
firstPublishedAttimestamp.
Until now a row only said what it IS. Unpublishing sent it back to draft and erased every trace it had ever been public, even though the inbound links, feeds and search results it collected while live were still out there. Anything that needs to ask "was this address ever public" had nothing to read.
The value is set once, on the first transition into published, and never changes afterwards: it is the date of the first publication, not the most recent one. It survives an unpublish, and it stays empty for an entry that has only ever been a draft. Entries that already existed keep an empty value, because whether they were once published was never recorded and cannot be recovered after the fact.
Collections and singles without Draft/Published do not get the column: they have no unpublished state, so there is no transition to record.
For a collection translated into several languages, the value answers whether the document has been public in any language, since every translation shares one address. Publishing a single translation therefore records it.
The value is set by Nextly alone. A firstPublishedAt sent in a create or update request is ignored, so the recorded date is always one that actually happened.
- #491 `c78afca` Thanks @mobeenabdullah! - When a service raises a typed error, the public result shape drops its
causeandlogContextbefore the boundary rebuilds it, so an operator saw a generic reconstruction with none of the detail the thrower attached. The original is now kept for the request and logged against the samerequestIdthe response carries, so the two can be joined.
An error response can also carry a _devDiagnostics field with that detail, so an author sees why a request failed without reading the server log. It requires TWO signals: NODE_ENV=development AND NEXTLY_DEV_DIAGNOSTICS=1. Set the second in your local env file to switch it on. Neither alone is enough, because Nextly ships pre-built and stays external to your app build, so NODE_ENV is read at runtime and a production deployment started with the wrong value must not be able to disclose it. Production responses are unchanged either way.
- #517 `089a758` Thanks @mobeenabdullah! - Two corrections to how the page builder's isolation check reads names, both of
- which made it reject stylesheets that were correct.
A font family is matched without regard to case, so a namespaced family spelled in capitals is the same family; a keyframe or a layer name is case-sensitive and still is. A comment is whitespace, so a comma inside one no longer splits one name into two.
- #487 `41d7c8d` Thanks @mobeenabdullah! - Localization migration files now record what transition they are for.
nextly migrate:create writes an extra header line on each _locales companion migration naming the transition, the kind of entity it belongs to, and the columns involved. Nothing reads it yet, so applying a migration behaves exactly as before, and files generated by earlier versions keep applying unchanged.
- #518 `1797d27` Thanks @mobeenabdullah! - Record a
login-succeededaudit event when a session is issued.
Failed logins have been recorded since the audit log shipped; successes were not. A trail of failures alone shows that someone tried and not whether they got in, which is the first question asked after a credential leak.
The event is written where the session is issued, not where the flow began. Three handlers issue sessions — password login, second-factor resolution, and the forced first-sign-in password change — so recording it in the login handler alone would have left every user who completes a second factor absent from the success trail, which is the population most worth seeing in it. Recording on an HTTP 200 instead would have the opposite fault: the challenge and password-change legs answer 200 while issuing no session, so a success would be reported for an account that was never reached.
It is recorded last, after the post-login hooks. A hook that throws sends the handler into its failure path, which returns an error and records a failure, so the client receives neither the token body nor the cookies — a success recorded before that point would leave the trail asserting both outcomes for one attempt. Those hooks now run inside the same shared step for that reason: all three handlers ran the identical pair, and the order between them decides whether the trail can contradict itself.
Unlike the failure event it is attributed to the account. Naming the account on a failure is the account-state leak the unified error response exists to avoid; on a success it is the whole value of the record.
Setup records it too. Creating the first administrator hands out a working session without going through the shared login path, so that account — the super-admin — was the one login absent from the trail.
Also fixes an overstated token expiry on the login and setup responses. The
expiresAt they return was derived from a fresh clock reading taken after the
awaited work that follows signing, so it named a later moment than the token's
own exp claim. signAccessTokenWithExpiry now returns the token together with
the expiry it actually carries, computed once and set explicitly, so a caller
reports the truth rather than a parallel calculation that drifts by however long
that work takes — unbounded, since plugin afterLogin hooks run there.
- #477 `302264b` Thanks @mobeenabdullah! - Field-level read access on an expanded relationship now applies to each related row before its parent's
afterReadfield hooks run, matching a direct read. Previously a parent hook was handed a nested child with the caller's denied fields still present, so a hook that copied such a field onto an allowed key exposed it under that key even though the child's own field was redacted afterward.
Behavior change: a field afterRead hook can no longer observe a related row's caller-denied field, so it can neither leak nor mask on one. A value that must stay hidden should be protected with an access.read rule keyed on the caller rather than a hook that reads another field the caller cannot see. Trusted reads (overrideAccess) are unaffected, since field access is skipped for them.
- #499 `1825c8f` Thanks @mobeenabdullah! - Catch every spelling of a Field Group field name that collides with one of its table's system
- columns, not only the two that were listed.
CreatedAtreaches the samecreated_atcolumn as -
createdAtdoes, and was accepted. Names are now compared as the column they become, so a field - declared with a plugin-contributed type is checked too — its type registers after the config is
- read, and it was previously skipped.
A Field Group field that references another Field Group may take any name that a Field Group
instance does not already use for itself: not id, which is the instance's own identity, and not a
name that converts to created_at or updated_at, which a read would fill with the row's
timestamp instead of the referenced data.
- #516 `00fee42` Thanks @mobeenabdullah! - Breaking (plugin authors):
ctx.services.collections.createEntry,updateEntryand -
deleteEntrynow resolve to{ message, item, warnings? }instead of the bare row.
This is the same envelope the Direct API and the REST API already return, so the same failure is
equally visible however the write was made. Previously a plugin was the ONLY caller of a write
that could not see a post-commit hook failure: afterCreate / afterUpdate / afterDelete run
once the row is durable, so a handler failing there cannot un-save it — the write reports success
and the failure travels beside it as warnings. The plugin facade never opened a collector, so
those failures were invisible to the plugin that caused them.
Migration is one property access:
// Before
const post = await ctx.services.collections.createEntry(slug, data, {
as: "system",
});// After
const { item, warnings } = await ctx.services.collections.createEntry(
slug,
data,
{ as: "system" }
);
item.id;
if (warnings)
ctx.logger.warn("side effects failed", { id: item.id, warnings });
`
deleteEntry reports item as { id }, since there is no row left to return. Reads
(listEntries, findEntryById, count) and createMany are unchanged.
- #483 `326ac0d` Thanks @mobeenabdullah! - A hook that throws in a post-commit phase (
afterCreate/afterUpdate/afterDelete) now reports the failure to the caller instead of only to the server log. The write still reports success, because the row is durable and a side-effect phase cannot change it, but the result carries awarningsarray naming the phase, the entity and the error code so an integration can react to a side effect that did not run. The field is present only when something failed, so an ordinary response is unchanged. It appears on the REST mutation and bulk envelopes and on the Direct API'sMutationResult,DeleteResultandBulkOperationResult.
Breaking (Direct API): nextly.updateSingle() now returns the same { message, item } envelope the collection mutations return, instead of the bare updated document. Singles run the same post-commit phases as collections, so this is what gives their hook failures somewhere to be reported — and it removes the one mutation that did not report its outcome like the others. Read the document from .item:
// before
const settings = await nextly.updateSingle({ slug: "site-settings", data });// after
const { item } = await nextly.updateSingle({ slug: "site-settings", data });
item.siteName;
`
- #511 `51d2469` Thanks @mobeenabdullah! - A failure now chains the error it actually came from onto what the caller receives, through
- every boundary that rebuilds one: REST routes, the Direct API, the singles route, the
- plugin-facing collection facade, the bulk-by-query paths and the version writes. Previously
- only typed failures carried their origin, and only on the Direct API, so a connection drop or
- a constraint rejection arrived with nothing naming what actually went wrong. The status-derived
- rebuilds — a code-less 404, 403, 409 or 500, which is exactly what a raw driver rejection
- produces — dropped it too.
NextlyError.notFound, .forbidden and .conflict accept a cause alongside logContext,
matching .internal.
One place now builds the error response body, so plugin routes answer with what every other
route answers with. Three consequences for a plugin route:
- Failures now carry _devDiagnostics in development, which this surface never had.
- A handler that throws a non-NextlyError still answers 500, but the thrown error is now
chained onto it instead of discarded.
- A 401 or 403 now returns the canonical { error: { code, message, requestId } } body with
application/problem+json, matching the rest of the API. It previously returned the legacy
{ data: { ... } } body with application/json, so a single plugin route answered rejected
requests and failing handlers in two different shapes. A client reading a plugin route's
auth-failure body needs updating; one reading the status or a handler failure does not.
- #497 `a4d86c1` Thanks @mobeenabdullah! - Harden nested field-level read access against
afterReadhooks that reshape a - read response. A related row's presentation is its own collection's authority, so
- the response's related rows are now rebuilt from the versions the read sanitized
- rather than inspected for tampering: whatever a source collection's
afterRead - hook did to a related row — reintroducing a denied field, cloning or reshaping the
- row, replacing, appending, reordering or removing its nested group/repeater rows,
- or returning a rebuilt document — is discarded. The rebuild runs after every hook
- phase, so one phase cannot hand the next a contaminated related row to copy from.
Closes a field-hook exfiltration path on related rows. A field hook belongs to one field but is handed the whole row, so a hook on an ALLOWED field of a related row could read a DENIED field beside it and return it as its own value — and the access pass that ran afterwards, judging each field by its own rule, had no reason to remove the copy. The target collection's field access now runs BEFORE its field hooks and again after, the same order a direct read of that collection uses: a row reached through a relationship may be redacted more strictly than the target's own endpoint, never more loosely.
Also fixes a related-row read-access gap for a relationship that declares a single
target as an ARRAY (relationTo: ["posts"]). That form stores and expands as the
discriminated { relationTo, value } pair, but the nested read decided the pair
shape from the NUMBER of declared targets and so treated the wrapper as the row
itself — evaluating the target collection's field access.read rules against an
object holding only relationTo and value, which matches nothing. A field the
target collection denies was returned inside the wrapper. The shape is now read
from how the target was declared, in one place shared by every reader.
This also removes the previous release's over-stripping: a related row a hook merely copied is no longer returned with its access-controlled fields denied, it is returned correctly sanitized, and the development-mode warning about reshaped rows is gone. A denied source field stays hidden from the source collection's own field hooks so it cannot be copied onto a selected field.
Notes for hook authors. A source collection's afterRead hook can no longer change
how a related row appears in the response, including its readable fields: transform
the related collection's own fields with that collection's field hooks instead.
Filtering or reordering a hasMany relationship still works, since that shapes the
source field rather than the related rows. A populated related row a hook invents
(one the read never expanded, so no collection's read rules were ever applied to it)
is returned as the bare reference it names rather than as an object.
- #486 `04fb6ab` Thanks @mobeenabdullah! - Style values are read more carefully in three places. A composite no longer
- builds an unbounded amount of issue text before its allowance is checked, an
-
attr()fallback is validated as the single value it substitutes rather than as - an arithmetic expression, and an expression is still judged where it can be even
- when part of it cannot be read.
- #501 `fcdcd2d` Thanks @mobeenabdullah! - The style compiler now accounts for every shape of persisted data it cannot use.
- A state map, a breakpoint map, a
visibilityenvelope or itsdevicesmap that - is not an object applies nothing, and each is reported rather than skipped, so a
- document with values and a page with no CSS are always connected by a warning.
The node walk is bounded by what it READS rather than by what it could use, so an array of malformed entries can no longer pass the node cap without tripping it.
- #492 `379c16a` Thanks @mobeenabdullah! - The engine can now compile a page's stored styles into CSS.
compilePageCss - turns a document and its site context into one stylesheet plus the class each
- node should carry, reading only persisted data: styles are never gathered while
- something renders, so a block cannot lose its styling by not being on screen
- when the sheet was built.
Design tokens compile to the custom properties they read, logical values stay
logical so one stored style is correct in both reading directions, states
compile to :hover, :focus-visible and :active, and both breakpoint axes
compile to media and container queries. The same document always produces the
same bytes.
States are emitted inside :where() so they add no specificity, and every rule
is decided by source order instead: a node's own value beats its block type's
default at every width, and a value set for a state beats a base value set at a
narrower breakpoint.
A value the validator refuses is left out of the stylesheet and reported rather
than written, whether or not the caller validated first. The same holds for
everything the compiler cannot act on: a block type that is not a namespaced
slug, a style state it does not recognise, a breakpoint id that resolves to more
than one definition, two nodes sharing an id, and a malformed envelope are all
left out and named. StyleCompileContext takes the document limits, so the
node walk stops where validation would have.
- #503 `387e593` Thanks @mobeenabdullah! - Stylesheets compiled by
@nextlyhq/blocks-enginenow sit one specificity notch - higher, so ordinary site CSS no longer beats a value set in the builder by
- accident. A rule like
.content .card h1used to win over a block's own colour - and leave the author with a style that silently did not appear.
This applies to that engine's output. @nextlyhq/plugin-page-builder renders
through a compiler of its own that does not yet follow these weights, so pages
rendered through it are unchanged by this release.
Overriding on purpose still works: an unlayered selector that beats the builder's
specificity wins, and so does !important, because the compiler deliberately
never writes it. Two things are worth knowing.
If your CSS lives in a cascade layer, as Tailwind's does, layer order is settled
before specificity and the builder emits an unlayered stylesheet, so adding
classes inside an @layer will not win. Write the override unlayered, or use
!important.
If the property you are overriding is mid-transition, the transitioning value
outranks every author declaration including !important until the transition
ends. Add transition: none !important to your rule if that applies.
- #466 `4dc8a46` Thanks @mobeenabdullah! - Add the style-property catalog to the blocks engine: the set of style properties a block may set, each with its value shape, the CSS it emits, and the design tokens it accepts. Storage keys are logical, so one page renders correctly in both left-to-right and right-to-left languages without a separate copy. Style values are checked for safety and for being the kind of value their property takes, before they reach a stylesheet.
The built-in block supports sub-flags now match the catalog. A block declaring spacing.blockGap, color.background, or border.width/style/color will fail to register and must use the group's current flags instead; the error names them.
- #488 `a4c6092` Thanks @mobeenabdullah! - Document validation can now check design-token names and class ids against the
- site that will render them. Both are optional: validation is given the site or
- it is not, and without it these names are not checked at all. An unresolved name
- is always a warning, never an error, so renaming a token or retiring a class
- never makes a stored document unpublishable — including when a rename leaves
- more unresolved names than one report can carry, which is now said separately
- and does not stop the checks that decide whether a document is valid.
- #494 `9653096` Thanks @mobeenabdullah! - Reject a Schema Builder field named
createdAtorupdatedAtwhen the name is chosen, rather - than letting it fail later as a database error. Both snake-case onto a system column and land in
- the same
CREATE TABLEtwice, so a collection carrying one could never be created.
Internally, what a system column is now lives in one declaration per column instead of ten hand-written lists across the codebase, so a column added in future reaches the schema, the write paths, the response shapes and every validator at once.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.50 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #436 `5e64acc` Thanks @mobeenabdullah! -
beforeOperationhooks are now declared and registered as what they are. They receive the operation'sargs-- the data, id or where clause it is about to use -- rather than a document, so they are typed asBeforeOperationHandlerand registered throughregisterBeforeOperation()/registerBeforeOperationHook(). Previously they were declared as ordinary hook handlers, so a handler written against the documented type readcontext.dataand gotundefined. Handlers for the other eight phases are unaffected.
- #455 `80fdee6` Thanks @mobeenabdullah! - A block that supplies its own editor component now loads without a hand-written import.
A block can name a custom inspector or canvas component through editor.component. That is a component path like any other admin contribution, so it now goes into the generated admin import map alongside plugin pages, settings and views — the editor bundle picks it up with no host wiring.
Paths are read from what plugins declare, so generation needs no plugin to boot. A block registered imperatively at runtime contributes no path, the same rule the block manifest follows. An app whose only components come from blocks now gets an import map too, where before none was written.
- #450 `7a36ab6` Thanks @mobeenabdullah! -
nextly generate:typesnow writes a block manifest listing every block your plugins declare.
Until now the only way to ask what blocks an app has was to boot it and inspect the registry, which is not available to an editor build, a docs page, or an agent writing a page document. The manifest states it as a file beside your generated types: each block's name, schema version, description, worked example, prop schemas, style capabilities, slots, and the plugin that declared it.
It is written from what plugins declare rather than from the running registry, so generation stays a pure read of your config: no plugin boots and no database opens. Blocks registered imperatively at runtime are not listed, because they cannot be known without running the plugin. No file is written when nothing declares a block.
- #476 `6cb97df` Thanks @mobeenabdullah! - Collections and singles created through the Schema Builder now get their system columns from the same definition the runtime schema and the migration diff already use, instead of a separate hand-written copy.
The copy had drifted. A Builder-created table declared createdAt and updatedAt as required while the rest of Nextly described them as optional, so nextly db:sync proposed a change to those columns on every Builder collection, and applying it rebuilt the table. On SQLite that rebuild also dropped the timestamp defaults. Both now agree, and the sync proposes nothing.
Newly created Builder tables declare the two timestamp columns as optional. Existing tables are brought in line by one schema sync, which preserves their rows.
The practical effect is that a system column added to Nextly in future reaches Builder-created tables as well as code-first ones. Previously it reached only code-first tables, and reading a Builder collection or single failed with a missing-column error.
- #480 `3b39129` Thanks @mobeenabdullah! - A dev-server config reload now applies hook edits only when the reload advanced the runtime in every dimension. Previously a reload that applied part of a config — one collection's schema change refused while others landed, or a field-tree sync that failed for a scope — could still publish the new handlers, leaving them running against tables and serialized field metadata the save had not reached. A hook edit that shares a save with a refused schema change now takes effect on the next save instead; a hook edit on its own changes no table, so it still applies immediately.
- #441 `55d3aa6` Thanks @mobeenabdullah! - A plugin can now add its own blocks to the page builder.
The page builder exposes its block registry as a service, and a contributing plugin reaches it from init with blockRegistry(ctx).register(myBlocks). Registering this way rather than by importing the engine is what makes the timing safe: the block registry is cleared and rebuilt on every boot, so a direct call can land before the rebuild and lose the blocks with no error, while services are recorded before any plugin's init runs. Each block is attributed to the plugin that registered it, taken from that plugin's own identity, so a name collision names the packages actually responsible.
defineBlock and the block types come from @nextlyhq/plugin-sdk/blocks, keeping the SDK the one stable surface a plugin author imports from while a plugin that has nothing to do with blocks never pulls the engine into its type graph. The registry itself comes from @nextlyhq/plugin-page-builder/blocks, since it belongs to that plugin rather than to core. Custom supports are registered through the same service as blocks, so both share the per-boot reset and neither collides on a second boot. Nextly core is unchanged: it carries no blocks contribution key and does not depend on the block engine, because contributing blocks is contributing to the page builder rather than to the framework.
- #464 `a3b1f48` Thanks @mobeenabdullah! - Editing a published entry on a drafts-enabled collection now works as a proper draft and publish flow.
When a collection has drafts enabled, editing a published entry saves your changes as a pending working draft instead of overwriting what is live. The editor shows a "Changed" status while a draft is pending, a Publish button promotes it to the live document, and a confirmed "Discard draft" action throws the pending edits away and restores the published version. The read API also surfaces the working draft to a trusted editor through ?draft=true.
- #428 `341890f` Thanks @mobeenabdullah! - You can now edit a published document without changing what visitors see.
Saving changes to a published document (without choosing Publish) now keeps them as a pending draft: the live version stays exactly as it was until you publish. Clicking Publish brings the whole pending draft live at once, including fields the Publish action itself did not resend, and Unpublish does the same in reverse while returning the document to draft. Trusted editors see their pending edits when they open the document; anonymous and published-only reads always get the live version. This applies to non-localized collections that have draft/published status with drafts-enabled versioning; localized collections are unchanged for now.
- #451 `9586432` Thanks @mobeenabdullah! - Add a
draftread option to fetch a document's pending working draft.
nextly.findByID({ collection, id, draft: true }) and the REST ?draft=true query parameter now return a published document's pending working draft in place of the live version. Access is gated on edit capability: a caller who cannot update the document still receives the published version, so this never exposes a draft to a read-only reader. Only non-localized collections with draft/published status and drafts-enabled versioning have a working draft to return.
- #434 `b8c4941` Thanks @mobeenabdullah! - Groundwork for the field group storage migration. The engine can now plan a complete run in either direction and resume one that was interrupted. A rename also carries the pointers that address the table it moves: a field group nested inside another records its parent by physical table name, so renaming the parent without rewriting those records would leave the nested content in place but unreachable, and reads would return nothing rather than fail.
Nothing runs it yet. No command invokes the migration and no database is changed by installing this; the entry point ships separately, once the engine is covered end to end against real PostgreSQL, MySQL and SQLite servers.
- #463 `a8f7a78` Thanks @mobeenabdullah! - A write refused inside a field group is now reported as the refusal it is. A blank required field returned a generic server error with no per-field detail, because every dialect adapter re-classified anything thrown out of a transaction as a database failure — including an error the application raised deliberately to roll the write back. Collections were affected on create and update; singles already behaved correctly.
- #459 `5d962d2` Thanks @mobeenabdullah! - Field validators inside a field group now receive the write's request context, so a plugin field type whose rule depends on
req.userbehaves the same nested in a field group as it does at the top level. Previously that rule saw an empty context and accepted every value.
Adds nextly generate:manifest, which emits the block manifest on its own, and --check, which writes nothing and fails when the committed manifest no longer matches the config. The manifest also publishes its own schema, and generation now refuses to write a document that schema would reject.
- #469 `13e3578` Thanks @mobeenabdullah! - Schema applies and the
nextly migrate/nextly upgrade --reconcile-corecommands now address the field-group registry and each field group's storage by the names the database actually holds, instead of the names this release would have created. Without this, a database whose field-group storage had been renamed could have an empty second registry created beside the populated one, after which the app would read the empty one and its field groups would appear to be gone.
- #472 `84f8a15` Thanks @mobeenabdullah! - The field-group storage migration now re-checks the ledgers it rewrote before it settles, and refuses rather than reporting success when a row still carries the old vocabulary. Without this, content written while the migration was running could be left in the old format and the run would complete silently, with the problem only appearing in a much later release.
- #454 `11f75b5` Thanks @mobeenabdullah! - Field-group storage is now addressed by the name the database actually holds.
The storage migration renames the field-group registry table and each data table's type discriminator. Every reader resolves those names from the database catalog instead of a constant, so a database that has run the migration and one that has not are both read correctly by the same build. Nothing about stored data changes, and a database that has not migrated behaves exactly as before.
- #429 `151efce` Thanks @mobeenabdullah! - Turning localization off in
nextly.config.tsnow brings your content back onto the main table. Previously only the Schema Builder toggle did this, so settinglocalized: falsein configuration left every translation in a table nothing read any more and fell back to whatever the entity held before it was localized. Turning localization on again no longer trusts the stale rows that companion still holds.
Enabling localization and Draft/Published in the same edit now applies. It used to fail part-way and could never succeed on a retry, because the copy read a status column the schema push had not added yet.
Saving a localized entity is faster, and on PostgreSQL a class of failure is gone. Every localized write used to ask the database whether each translation table existed — once per entity, plus once per field-group type in the payload, before the write and again inside it. That answer is now resolved once and remembered. The read that builds the response used to discover the same thing by running its query and catching the failure, which on PostgreSQL aborts the whole transaction: writes that should have succeeded failed with current transaction is aborted, blaming an unrelated statement.
When a translation write is refused, the message now names the right fix for where you are running. Production is told to run nextly migrate instead of nextly db:sync, which is a development tool and cannot help there — and nextly migrate now creates missing translation tables and repairs installs that enabled localization before Nextly began recording it.
Turning localization off now brings an entry's publishing state back with its content. Publishing is per language while an entity is localized, so an entry published only under a language that is no longer your default carried that state on its translation row alone — and restoring the content without it could put a draft in front of the public, or make live content disappear.
Two processes enabling localization for the same entity at once — a db:sync alongside a running dev server, say — no longer both do the work. Only one holds the transition; the other stops and says so, instead of racing to seed the same rows or overwriting translations written since the first one finished.
If you open your own transaction and call createEntryInTransaction / updateEntryInTransaction / deleteEntryInTransaction (or their batch equivalents), call warmLocalizedReadiness(collectionName) before you open it. Nothing fails if you do not, which is why it is worth knowing: the write commits, but the version history it records and the webhook event it sends will be missing every translated value from your localized components.
- #452 `6536365` Thanks @mobeenabdullah! - Turning localization off now brings an entry back with the publishing state it was actually published under. Publishing is per language while an entity is localized, so an entry published only under a language that is not your default carried that state on its translation row alone — and the disable drops that table straight after restoring, so the state was lost for good. A draft could become publicly visible, or live content disappear.
- #440 `ed94b78` Thanks @mobeenabdullah! - Plugin options on a code-defined user field are no longer refused when two of them share a reference, and a sparse array in them is now rejected rather than silently reshaped.
The JSON-shape check treated every object it had already visited as a cycle, so one object referenced from two places within a single option was refused even though it serializes correctly at both. It now tracks only the objects on the active path. It also walked arrays with a method that skips holes, so a sparse array passed the check and then had each hole written as null, handing the plugin's component different data than was declared.
- #442 `5785ee5` Thanks @mobeenabdullah! - Apply read hooks per collection, and hand them the values a caller sees.
A read hook that reads a different collection now runs that collection's own hooks instead of silently skipping them, so a hook cannot reach rows the other collection withholds. A hook reading the collection it is already running for still skips them, which is what stops it calling itself without end.
afterRead is now handed decoded JSON values rather than the storage encoding
SQLite returns, so a hook reads the value the field was configured with instead
of a string. Field hooks are also declared with the context they are actually
given, which includes the field's value and name.
- #468 `375d796` Thanks @mobeenabdullah! - On MySQL, the internal description of a collection table's
created_atandupdated_atcolumns said they had no database default, while the tables actually created for them do have one (CURRENT_TIMESTAMP). The schema comparison that decides what a migration should contain was reading the description rather than reality, so it could see a difference that was not there. The description now matches what is created.
- #460 `cbaa8d8` Thanks @mobeenabdullah! - Run collection and single
beforeChangehooks after validation, not before
A beforeChange handler declared on a collection or single used to be
registered onto the beforeCreate/beforeUpdate queue, which fires before the
schema rules are enforced. The phase documented as the last chance to shape a
stored value therefore ran on data that had not been validated, and it ran even
for writes that were about to be rejected. The field-level hook of the same name
was already in the right place, so the two beforeChanges meant different
moments.
beforeChange is now its own phase, executed immediately after the validation
gate on every write path: collection create and update, both of their
transactional forms, the transactional single paths, and the single update
service.
Singles gain beforeValidate, which they did not have. Moving beforeChange
past the gate would otherwise leave a single with no hook running before
validation at all, so the phase takes the pre-validation execution point
beforeChange vacated. A single and a collection now agree on both phases.
This changes when existing handlers run. A beforeChange that SUPPLIES a value
the schema requires now runs too late to satisfy it, because validation has
already been applied; move that work to beforeValidate, which runs before the
gate on collections and singles alike. This includes the Schema Builder's
pre-built "Auto-generate Slug" hook when it targets a required field of your
own. The framework's own slug/title derivation is unaffected: it does not
run as a hook.
What a beforeChange handler returns is written without being re-validated.
That is the point of the phase, and it is now true rather than accidental.
- #443 `bdcde29` Thanks @mobeenabdullah! - Clear the hook registry when services shut down.
The registry is process-global and outlives the DI container, but handlers are registered from config on every init. Re-initializing in one process therefore left the previous instance's handlers in place and appended a fresh copy of each, so every hook ran twice per operation and the dead instance's handlers ran alongside the new ones.
- #467 `8a4d4a3` Thanks @mobeenabdullah! - Bind the Direct API for hook contexts at registration
req.nextly is now bound for hook contexts from the moment services are
registered. It previously resolved through a binding that getNextly() created
as a side effect of its first call, so a process that never called it — which is
any REST or admin write — handed every hook undefined, including the worked
example in the collections guide.
- #473 `9dfbd80` Thanks @mobeenabdullah! - Apply hook edits without restarting the dev server
Editing a hook in nextly.config.ts had no effect until the process restarted,
and deleting one left it firing. A config reload re-read the file but the
registry kept the function objects registered at boot, so the hook that ran was
always the one from startup.
Collection and single hooks are now rebuilt from the reloaded config. Clearing
them is safe because the registry records who registered each handler: a
reload replaces only what it can rebuild, and leaves alone both a plugin's hooks
(the form builder registers directly on forms, and plugins do not re-run on a
config reload) and any registered imperatively through registerHook() (nothing
re-runs those at all). Unregistering is likewise scoped to the caller's own
registrations, so a plugin removing a handler it shares with the config no
longer removes the config's instead.
A save that changes a hook and a schema at once is handled as one unit: the new
handlers are published only once the schema they were written against has landed,
so a request served while the reload is still running never sees a hook reaching
for a column that is not there yet, and a refused schema change leaves the
previous handlers in place. Replacing them also keeps their position, so a config
save no longer reorders a chain it is not changing. Switching a plugin to enabled: false
now stops everything it contributed -- the hooks its collections and singles
declared, and the ones it registered itself, which are suspended rather than
dropped so re-enabling it in the same session brings them straight back. Deleting
or renaming a collection stops its hooks too: a removed entity's table is kept until nextly prune, so it stayed
addressable and went on running hooks its config no longer declared.
Deleting a plugin from the config stops its hooks as well as disabling it does, and a plugin that was disabled stays that way when it is later removed.
Registering straight into the registry that getHookRegistry() hands out now
marks the handler as the app's, matching registerHook(). Only the registrars
that read the config claim ownership a reload may replace, so a handler nothing
can rebuild is never removed by one.
- #445 `d20e9d3` Thanks @mobeenabdullah! - Keep a typed error's status and code across the service boundary.
A service raising authRequired, rateLimited, serviceUnavailable or any
other 401 reached a REST caller as a generic 500, because the boundary rebuilt
errors from their HTTP status and only four statuses had a branch. A 400 was
rebuilt as a validation failure whatever code it carried, so a caller was told
its data failed validation when it had not been validated.
Errors are now rebuilt from the canonical code the envelope already carried, with the status mapping kept as the fallback for envelopes that carry no code.
- #449 `3dc6927` Thanks @mobeenabdullah! - A field masked by its collection stays masked when read through a relationship.
A field's afterRead hooks are how it masks itself on the way out, and they ran
only when the collection was read directly. Reaching the same row through a
relationship returned the unmasked value. They now run over the assembled
document, so a nested row gets its own collection's treatment at every depth,
and a hook that masks based on the row's own relations sees them expanded
rather than as raw ids.
- #446 `4e5064e` Thanks @mobeenabdullah! - A plugin can now declare data for another plugin statically, and the page builder registers contributed blocks from it.
contributes.declarations is the static counterpart to contributes.services. A service is a factory, so what it provides is knowable only once a plugin has booted — and nextly generate:types boots nothing, reading the config alone. A capability offered only through a service is therefore invisible to generation and cannot appear in generated types, an import map, or a manifest.
A block contributor can now declare its blocks instead of registering them by hand, and the page builder registers them at boot from the same declaration the tooling reads, attributed to the plugin that declared them. Registering imperatively from init still works for a plugin whose block list depends on runtime state.
- #438 `7b0dddf` Thanks @mobeenabdullah! - The page builder now states the core version it actually needs, an empty default is checked against the column it will occupy, and a user field's plugin options are refused when JSON cannot hold them unchanged.
@nextlyhq/plugin-page-builder requires nextly 0.0.2-alpha.49 or newer, the release that first exports pluginField. Installed against an older core it now fails at install rather than throwing when a blocks() field is evaluated.
A single's default that resolves to an empty value is validated against the field's storage primitive and its type's own rules, instead of being treated as a field the writer left alone; a number-backed default of "" no longer reaches the insert. Options declared on a code-defined user field are refused when they are values JSON cannot represent — a Date, Set, Map, BigInt, function or cycle — which previously either reached the admin component reshaped or failed the whole startup sync.
- #435 `082fa67` Thanks @mobeenabdullah! - Plugin field types now work on every surface that accepts fields, and a column added to an existing table gets the same storage class the ORM binds.
A contributed field type can be declared in contributes.extend and defineFieldGroup, not just in collections and singles, and pluginField() keeps the shape it was given so a plugin's own factory stays typed. The page builder exports isBlocksField again and reaches core only through @nextlyhq/plugin-sdk, which now carries the field contracts a contributed type needs; it also states the core version its blocks() factory actually requires, so installing it against an older core fails at install rather than at runtime.
A contributed default is checked against the type's storage primitive before it reaches the database, disabling a plugin no longer leaves its empty-value callback registered, and nextly build and migrate:check now refuse a field type no installed plugin offers instead of generating types for a schema production would reject. Field names are validated even when the field's type is deferred to boot, so a duplicate or SQL-reserved name can no longer reach schema generation.
Plugin options declared on a code-defined user field are persisted and reach the contributed admin component, and a number field added to an existing table is created as the integer the ORM binds rather than NUMERIC/DECIMAL/REAL, honouring dbType: "decimal" and format: "float" for fields that ask for fractions.
- #456 `1bc29b5` Thanks @mobeenabdullah! - Editing a published entry's title no longer changes its URL. The slug follows the title while an entry is still a draft and stops once the entry has a public address, at which point it changes only if you edit it yourself. Previously a title edit silently retired the published address and every link to it started returning a not-found page.
An entry counts as publicly addressed in three cases, each of which was a way to lose a URL: it is published wherever its slug is served; it lives in a collection with no draft/published lifecycle, where saving is publishing; or you have published it at least once while the editor has been open, so unpublishing to make an edit does not put the address back up for grabs.
Where the slug is served depends on the slug field. The slug a collection gets by default is shared across languages, so one address serves all of them and any published language keeps it frozen: editing the title of a German draft no longer rewrites the URL the published English version is being served at. A slug you have explicitly localized is genuinely per language, and follows only that language's status.
When you do change a public entry's slug, the editor says so before you save: the public URL changes and the old one stops working. That notice now also appears in the quick-edit form opened from a relationship field, and it clears once the change is saved rather than lingering against the URL you already replaced.
- #439 `f2c6e97` Thanks @mobeenabdullah! - Read hooks now shape the query they precede.
beforeOperationreceives the caller's ownwhere(it was handed an empty one),beforeReadreceives whatbeforeOperationsettled on, andbeforeRead'sreturn narrows the rows the read returns instead of being discarded.countEntriesruns the same chain, so a total describes the same rows a list would return rather than counting rows the list withheld.
- #474 `7c3b9f2` Thanks @mobeenabdullah! - On SQLite, the
createdAtandupdatedAtcolumns of collection and single tables now carry a database default, matching PostgreSQL and MySQL and matching what the Schema Builder has always created.
Nextly sets both on every write, so content created through the admin panel or the API is unaffected. The difference shows up for rows written another way, such as a direct insert or a data import: on SQLite those stored no timestamp at all, and the value read back as null.
Existing SQLite tables pick the default up on the next schema sync, which rebuilds the affected tables in place and preserves their rows. Rows that already hold a null timestamp keep it, because a default applies only to inserts that omit the column.
- #481 `e604c52` Thanks @mobeenabdullah! -
createTestNextlyno longer resolves the Direct API while building its return value.t.nextlyis now resolved when it is read. Resolving it registers thenextlyDirectAPIcontainer binding as a side effect, and that binding is where a hook'sreq.nextlycomes from, so the old eager call meant the binding always existed under the harness whatever the code under test did. Property access is unchanged for callers; a test that wants to assert something aboutreq.nextlyshould do so before readingt.nextly.
- #479 `f7fb1fb` Thanks @mobeenabdullah! -
nextly migrateno longer fails outright when a localized project's companion table already exists but holds no rows yet, which is what a dev-server boot leaves behind. A project whose companion was already filled bydb:syncstill needs the follow-up fix tomigrate:create.
- #447 `ab6795f` Thanks @mobeenabdullah! - A hook that throws after the write has committed no longer fails the write.
afterCreate, afterUpdate and afterDelete run once the row is durable, and
a throw there reported the operation as failed with no entry returned. Callers
could not learn the id of the row that existed, and a retry wrote it a second
time. These phases now report their failures instead of raising them: the
operation succeeds, the error is logged with its phase and collection, and the
remaining handlers still run. beforeCreate and the other pre-write phases are
unchanged -- refusing a write is what they are for.
- #475 `f75c29f` Thanks @mobeenabdullah! - The field-group storage migration now re-checks the collection, single and field-group registries before it settles, so a definition saved while a run is in flight can no longer leave a database reporting success over storage that is only partly migrated.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.48 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #417 `1f81cf3` Thanks @mobeenabdullah! - Read every related row through one code path, so a capability added to relationship population applies everywhere a relationship is populated instead of at whichever call sites were remembered.
- #423 `2f05141` Thanks @mobeenabdullah! - Fixes PostgreSQL index introspection reading indexes from the wrong table. Table names are unique per schema rather than per database, so a table with the same name in another schema had its indexes merged into the one being inspected. That could hide an index that needed creating, or report one that was never there.
Refuses to run a schema sync while a field group storage migration is in flight. Mid-run some tables carry their old names and some their new ones, and the registry rows pointing at them move one step at a time, so a sync during that window could delete storage it could not account for.
Also further groundwork for that migration: it can now execute its rename steps and check its own work. A table, its localization companion and the registry row pointing at them move as one step, and on PostgreSQL and SQLite they commit together. MySQL applies a schema change as soon as it is issued, so there the halves land in sequence and a resume completes whatever did not; a reader in that window sees a table as missing rather than reading anything wrong. Every step verifies against the database rather than trusting that it ran, and index survival is checked by name, so an index dropped and replaced by another is caught rather than passing on an unchanged count. Nothing calls the migration itself yet.
- #424 `0538f4f` Thanks @mobeenabdullah! - Let a form be updated without resending its fields. Changing a form's name or settings failed with "Form must have at least one field" because an absent
fieldsin the patch was treated as an empty one.
- #422 `a4dad07` Thanks @mobeenabdullah! - Emit a
form.submission.createdwebhook when a form submission is created. The event type was already subscribable in the admin UI but had no producer, so an operator could subscribe to an event that never fired.
Form submissions carry visitor-entered answers plus ipAddress/userAgent, so the submissions collection suppresses the PII-bearing entry.* events. It now instead emits a curated, metadata-only form.submission.created carrying only which form, when, and the status — never the answers, IP, or user agent. The event is recorded in the same transaction as the submission, so it commits atomically and is never delivered for a rolled-back write.
This is driven by a new declarative webhooks.emit collection option ({ event, fields }): any PII-bearing collection can replace its default entry.* events with a safe curated one that ships only an allowlisted set of fields (default-deny). The resource kind is derived from the event name.
- #421 `83ed5c9` Thanks @mobeenabdullah! - A string stored in a JSON field no longer fails the write on PostgreSQL and MySQL.
A field backed by a JSON column accepts any JSON document, a plain string included. A string that is not itself encoded JSON was passed through to the driver as bare text, which PostgreSQL and MySQL reject as invalid JSON, so storing "hello" in a json field failed the write outright. On SQLite, where the column is plain text, it was stored in a form no read could recover as what was written. Such a value is now encoded, so it round-trips as the string it was.
A string that already parses as JSON is still passed through untouched, so content a previous write encoded is not wrapped a second time.
- #415 `0e18a97` Thanks @mobeenabdullah! - Apply a target collection's read rule when it filters on one of that collection's localized fields, so populating a relationship returns the rows the rule permits instead of withholding every one of them.
- #405 `e1467e8` Thanks @mobeenabdullah! - Plugin-contributed field types are now first-class in generated output, in the manifest, and in the validation a plugin can reuse.
nextly build emitted nothing at all for a custom field type: the generators test membership of the built-in list, so the field was skipped while its value was still stored, leaving apps with no generated type and no schema entry for it. A type now states its own rendering through PluginFieldType.codegen, receiving the field as declared so a type whose options narrow what it stores can narrow what it generates.
A type's options can now be held in a pluginOptions container core never reads, so an option may use a name the field schema already declares — options, fields, admin, label — which was previously judged against the core meaning and refused. Options written directly on a field are still read, and a type is handed one flat view of both, so where an option was stored is not something a plugin author tracks.
A user field whose type a plugin contributed can now be declared from code with pluginUserField(), which was previously impossible without a cast: UserFieldConfig admits only the built-in shapes, and widening it to accept an unknown type token would have made a malformed built-in declaration pass too.
validateFieldValues is now available from the plugin SDK, marked experimental until a first-party plugin depends on it, so a plugin storing structured content of its own applies the same rules a write does rather than reimplementing required, the per-type checks, and every plugin field type's validate.
Several correctness fixes ride along, most of them about a value reaching a column its type cannot hold.
A JSON column stores a JSON document, and true or 42 is a document as much as {} is; only objects were encoded, so a scalar reached the driver as its own type and could not round-trip through a SQLite text column. The four write paths that each carried their own copy of that encoding now share one.
A value written to a custom user field was never checked against the column its type stores in, and a failed user_ext write is read as the extension table being absent — so the user was created without the value, with extensions disabled for the rest of the process, rather than the write being refused. Such a value is now refused with the field named. A required single field backed by a plugin type was seeded with the wrong kind of value for the same reason, which could stop the single being created at all.
nextly build now generates types for a project made only of singles, field groups or user fields, where it previously wrote nothing or left a stale file, and narrows PermissionSlug and EventName as generate:types does — a deployment build no longer widens types a development run had narrowed. db:sync --watch now keeps watching such a project too, instead of exiting its watch loop and never re-syncing.
A key named __proto__ was silently dropped when rebuilding an object from data nobody validates, which lost it from a delivered webhook envelope, from a stored version diff, and from the declaration a plugin validator judges. And db:sync --watch could classify one config's columns with another config's field types, because a reload replaces the process-wide registry while the previous sync is still running; work now resolves against the config it started from, and a reload whose watcher was replaced mid-flight no longer applies its result or leaves its registrations behind.
- #418 `21bb5b3` Thanks @mobeenabdullah! - Stop serving unpublished rows through relationships. A related row is now filtered by Draft/Published exactly as a direct read of it is, so a published document linking to a draft one no longer discloses that draft's contents.
- #414 `45faba9` Thanks @mobeenabdullah! - Emit
user.createdanduser.deletedwebhook events. Both were already advertised as subscribable in the admin UI but had no emit sites, so an operator could subscribe to events that never fired. They now record into the transactional outbox atomically with the account change, through a new Drizzle-transaction recorder (recordEventInTx) that lets services running onBaseService.withTransaction— like the auth service — participate in the outbox without the adapter's positional transaction context. Each event is attributed to the authenticated caller and, like the content write paths, offers the fast-path drain and a bounded retention prune after commit — including on self-registration — so delivery and outbox pruning do not wait for the scheduled drain. The delete event reads the removed account's identity inside the delete transaction, so a concurrent update cannot make it report a stale address. The payload is PII-safe: identity only (id, email, name), never the password hash, a token, or role assignments.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.47 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #404 `f41a985` Thanks @mobeenabdullah! - More groundwork for the upcoming field group storage migration: the rename plan is now derived from the database rather than from configuration, so a table named through
dbNameis found and left alone rather than renamed over. Nothing calls this yet, so there is no change in behaviour in this release.
- #408 `1448488` Thanks @mobeenabdullah! - Fixed component data teardown resolving table names case-insensitively on every database. On PostgreSQL, and on MySQL with
lower_case_table_names=0, two names differing only in case are two different tables, so a registered component whose stored name differed in case from a real table could have that other table's rows deleted. Whether two spellings mean one table is now read from the server rather than assumed, including that SQLite folds ASCII case only, soÄandästay distinct tables there.
Also more groundwork for the upcoming field group storage migration: the rename plan is now checked against what the database actually contains before anything runs, so a name already in use, a registry row whose storage or companion table is missing, or a half-applied rename that recorded progress cannot account for all refuse up front instead of failing partway through. That part is not called by anything yet.
- #382 `b448e6d` Thanks @mobeenabdullah! - Saving a translation could overwrite the original language.
nextly db:syncmarks a collection as localized in a separate process from the running app, so the app could show the language switcher before its translations table existed — and a translation saved in that window wrote over the original-language values and changed the entry's URL, while reporting success.
The translations table is now prepared during db:sync and during a dev config reload, for collections, singles and field groups alike. If it is still missing, a write in a non-default language is refused with a clear message instead of overwriting anything, and the same refusal now covers singles and embedded field groups rather than only collections.
Writing the default language before the table exists still goes to the main table as before. The one exception is content that was localized from the start, whose translatable values have never had a main-table column to fall back to: saving that while the translations table is missing used to fail with a database error, and now reports the same clear message as the case above.
Collections and singles that set a custom dbName are handled correctly here too; previously their translations table could be created against a table name that does not exist. And a database that is unreachable or refusing connections is no longer reported as a missing translations table.
- #401 `1e0ef91` Thanks @mobeenabdullah! - Stop a relationship from populating a row the caller may not read. A related
- row belongs to another collection and carries that collection's own read
- rules, but expansion selected it straight from its table and applied only
- field-level redaction — so a caller refused the collection outright still
- obtained its rows by populating a relationship that pointed at them.
The target collection's stored read rules are now evaluated for the caller before its rows are populated, on single reads, listings and nested hops. A refused target reads as an absent relationship rather than an error, so one unreadable reference does not refuse the whole parent read.
- #409 `d5568ff` Thanks @mobeenabdullah! - Consolidate the version-history reference access checks behind a single shared media/users read gate. Internal refactor with no behavior change: the media and users label lookups previously duplicated the scope-then-RBAC check inline, and now share one audited gate so every reference-resolution path stays access-checked.
- #406 `b7e334b` Thanks @mobeenabdullah! - Show linked entries and media by name in version history.
Previewing a past version or comparing two versions now shows relationship and upload fields by name: a relationship reads as the linked entry's title, and an upload as its filename with a thumbnail, instead of a bare id. Labels are resolved through the same access checks as a normal read, so a linked document you are not allowed to read stays shown as its id rather than revealing its title, and a many-relationship still shows the links the version actually held rather than the document's current ones.
- #410 `77fb550` Thanks @mobeenabdullah! - Polish version history for localized and restored content, and give the Schema Builder control over retention:
- - Filter version history by locale. The history panel now shows a language badge on each version and a locale filter (defaulting to all locales), so a localized document's history is legible instead of interleaved. The filter is added to both list surfaces (the REST route and the dispatcher) and hides automatically for non-localized documents.
- - Show restore lineage. A version created by restoring an earlier one now displays a "Restored from vN" chip on its row and in its preview, so a rollback is visible at a glance.
- - Set version retention in the Schema Builder. The versioning toggle's Advanced tab gains a retention control — keep all history, keep the default (50), or keep the last N per document — reaching parity with code-first
versions.maxPerDoc. The value persists through the builder's create/update endpoints and the committableui-schema.jsonmanifest.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.46 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #398 `4b46b5c` Thanks @mobeenabdullah! - Compare any two versions of a document in the admin history panel.
From a version's preview in the history panel, you can now compare it against the previous version or the current one. The comparison lays out what changed field by field: edited text reads inline with the added and removed words highlighted, changed values show their before and after, and list items and relationships are marked as added, removed, moved, or edited. A "Changed only" toggle, on by default, hides everything that stayed the same so the real differences stand out.
Available for both collection entries and singles on any document with versioning enabled. A comparison is always between two versions in the same locale.
- #403 `2685550` Thanks @mobeenabdullah! - Recover a version history that failed to refresh, without reopening the panel.
When the history panel cannot refresh its list (for example after the tab regains focus following a save made elsewhere), it keeps the loaded history on screen but holds back the "Compare with current" and "Load more" actions until it can confirm the latest version. It now shows a short notice with a "Try again" button, so a transient failure can be recovered in place rather than by closing and reopening the panel.
- #402 `b85b799` Thanks @mobeenabdullah! - More groundwork for the upcoming field group storage migration: a migration run now claims a durable lock row for its duration, so a second run refuses instead of starting alongside it, and records a step only after checking the database reached the state that step intended. Nothing calls this yet, so there is no change in behaviour in this release.
- #399 `831cf74` Thanks @mobeenabdullah! - Internal groundwork for the upcoming field group storage migration: durable progress tracking, and a startup guard that refuses to serve a database whose storage state cannot be accounted for. Nothing calls this yet, so there is no change in behaviour in this release.
- #383 `5154cc2` Thanks @mobeenabdullah! - Plugin-contributed field types can now state rules about their own declaration, not just about stored values.
PluginFieldType.validateOptions(field)runs on every path a declaration reaches storage by — boot,db:syncand its watcher, Schema Builder writes, the direct create/update endpoints,nextly build,migrate:create, and the HMR reload — and returnstrue, a message, or a list of issues naming the options at fault. Each of those sits after the field-type registry is populated; thedefine*calls do not, so a custom type is still refused there as an unknown field type. It reads the declaration as written, which on the Builder path means the submitted payload rather than the parsed copy, since that is what gets persisted.
Options a plugin field type declares now survive the Schema Builder. The admin rebuilt each field from a fixed list of known properties, so a custom option was dropped on the way in and again on the way out: saving an unrelated setting erased it from a field the user never touched, and a type that requires the option would have refused every save.
A config edit that arrives while a reload is already running is now read. Reloads still never overlap, but the one in progress may have read the file before the edit landed, so the edit was previously dropped until the next save or a restart. A config load that fails now also leaves the field-type registry as it found it, instead of leaving it empty for whatever keeps running on the previously-loaded config.
Without it a custom type's options were accepted unread, so a declaration that no value could ever satisfy was only discovered per write, which reports a schema defect to the writer who cannot fix it. A disabled plugin's declaration checks no longer run, matching its validate.
nextly build now runs the comprehensive config validators over singles and components, not collections alone. A single or component whose declaration was invalid previously reported a clean build and failed later at runtime.
- #384 `d2dabb9` Thanks @mobeenabdullah! - Populate relationships that point at several collections. A field declared with
- a list of targets stores its value as a
{ relationTo, value }pair, and - expansion treated that pair as if it were a plain id while resolving the table
- from the field's first declared target. The resulting query bound an object
- where the driver expected a string, failed, and the failure was discarded, so
- the field came back as its raw pair at every depth with nothing logged.
Values are now loaded from the collection each one names, on single reads, listings and nested hops alike, and a populated row is redacted by that collection's own field rules.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.45 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #389 `0c79043` Thanks @mobeenabdullah! - Field group REST endpoints moved from
/api/componentsto/api/field-groups, and the route re-exports fromnextly/api/componentsandnextly/api/components-detailtonextly/api/field-groupsandnextly/api/field-groups-detail. Apps that re-export these handlers must rename their route files and imports; the old paths are removed rather than aliased.
- #388 `711e0c5` Thanks @mobeenabdullah! - Generated types now use the Field Group vocabulary:
nextly generate:typesemits<Slug>FieldGroupinterfaces and aConfig.fieldGroupsmap, and the Direct API exposesFieldGroupSlugandDataFromFieldGroupSlugin place of theirComponentequivalents. Re-runnextly generate:typesafter upgrading so the generated file and these types agree.
- #392 `b51f4e8` Thanks @mobeenabdullah! - The admin panel now calls reusable field structures Field Groups. They live at
/admin/builder/field-groups(previously/admin/builder/components), and the navigation, dashboard tile, builder and list screens use the new wording. Bookmarks to the old admin URLs will not resolve.
- #386 `2eeef30` Thanks @mobeenabdullah! - Reusable field structures are now called Field Groups.
defineComponent()becomesdefineFieldGroup(), thecomponent()field helper becomesfieldGroup(), thecomponentsconfig key becomesfieldGroups, and plugins contribute them viacontributes.fieldGroups. The old names are removed rather than aliased, so configs must be updated on upgrade.
Stored data is untouched: tables, columns and the JSON written for existing content keep their current names, so this release moves no data and needs no migration.
Configs and plugins still using the old key now fail at startup with a message naming the new one, rather than starting up with those definitions silently unregistered.
- #390 `768bdc7` Thanks @mobeenabdullah! - The Direct API namespace
nextly.components.*is nownextly.fieldGroups.*, and the dashboard, plugin admin metadata, and plugin introspection responses report field groups under afieldGroupskey. Reading the old namespace now reports the rename instead of failing as an undefined property.
- #397 `663306a` Thanks @mobeenabdullah! - Internal modules and services for reusable field structures now use field-group naming. This renames three container keys reachable through the exported
getService():componentRegistryService,componentSchemaServiceandcomponentDataServicebecomefieldGroupRegistryService,fieldGroupSchemaServiceandfieldGroupDataService. The old keys are not aliased, so a call using one no longer resolves. The field group schema service also dropsgenerateSchemaCode(), an unused generator that was reachable through that same accessor. Stored data, table names, config keys and HTTP routes are unchanged.
- #385 `d135685` Thanks @mobeenabdullah! - fix(nextly): make version snapshots complete and safe to restore
Several version-capture gaps that could lose or corrupt content on restore are fixed:
- Restoring an old version captured the content it applied but not the content it replaced, so content written while versioning was off (held in no version) was destroyed on restore. The current document is now snapshotted as a "Before restore" version inside the restore transaction, protected by the existing retention logic. This covers both collections and singles.
- A single's component snapshots stored relationship and upload fields expanded into whole related rows instead of reference ids, so a versioned single with a component relationship could not be restored (the write failed) and could leak the related row's fields past redaction. Component snapshots now store references only.
- For a localized, status-bearing single restored at a non-default locale, the pre-restore snapshot recorded the main row's status instead of that locale's, so undoing a restore could publish content that was never published. The snapshot now records the restored locale's own status.
- A localized single's snapshot recorded only the fields a partial edit touched, dropping the write locale's other, still-persisted translations. The snapshot now carries the full set of the write locale's translations.
- Publishing every locale of a localized entry emitted only a single, document-wide entry.published, so a subscriber watching one language never heard its translation go live. Each companion locale that actually transitions to published now emits its own locale-tagged entry.published. The publish is also judged against the row read under its transaction lock, so it records nothing when the entry was deleted concurrently.
- #391 `962fd25` Thanks @mobeenabdullah! - Add a version comparison (diff) engine and endpoint.
You can now compare any two saved versions of a collection entry or a single and get a typed, field-by-field diff: word-level text changes, added, removed, moved, and edited items in repeatable and component fields (matched by their stable id, so inserting one row no longer marks every row after it as changed), and added or removed relationship targets. The diff is computed on the server and is access-gated and field-redacted exactly like reading a version, so a field you cannot read never appears in a diff. It is reachable over the dispatcher and as a standalone nextly/api/versions-diff route for both collections and singles. The admin comparison UI follows in a later change.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.44 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #374 `a44ab69` Thanks @mobeenabdullah! - Component tables are always derived from the component slug, resolved through a single canonical path. A custom
dbNameis no longer accepted ondefineComponentorcomponents.create(): it could name storage the component does not own, and whether two spellings refer to one table depends on database server configuration rather than anything the config can state. Components that relied on it should drop the option and let the table name derive from the slug.
- #380 `90108db` Thanks @mobeenabdullah! - Relationships nested one level deeper now expand for collections you defined in code, not only for those created in the Schema Builder.
?depth=2 promises to populate a related document's own relationships, and it did so only for Builder-created collections. Resolving a target collection's fields read one of the two shapes those collections are stored in, so a code-first target resolved to nothing, the recursion guard failed, and the second hop was skipped silently at any depth — you got a bare id where a document was promised.
Two consequences, both now closed. A depth: 2 read returns what it says it returns. And an access rule reading across two hops — data.author?.organization?.suspended !== true — was enforced on a Builder collection while being quietly unenforced on a code-first one, so the same rule over the same data gave different answers depending on how the collection happened to be defined.
Worth knowing if you use code-first collections with chained relationships: reads at depth 2 or more will now issue the queries that second hop requires, where previously they stopped early. Depth still bounds the walk, and a field's own maxDepth still overrides it.
- #379 `655532d` Thanks @mobeenabdullah! - Plugin-contributed field types can now validate what they store.
PluginFieldType.validate(value, { data, req, field, path, mode })returnstrue, a message, or a list of issues with their own paths. Previously a custom type could be invented but say nothing about what belonged in it.
Values of a custom type are now also checked against the storage primitive the type declares. A number-backed type used to accept the string "3" on its way to a numeric column, because the built-in rules only ever matched built-in type names; they now run first, then the type's validate, then the field's own. A disabled plugin's field types keep their schema but no longer run their validate, matching how every other plugin behavior is skipped.
json fields now reject a value JSON cannot represent — a cycle, a BigInt, a bare function — as a validation error naming the field, instead of letting it reach the driver and fail there as a server error. Values JSON merely reshapes, such as an undefined member, are still accepted. contributes.fieldTypes is documented for the first time.
- #367 `66053c3` Thanks @mobeenabdullah! - Honour a Single's declared hooks and field defaults
Two documented parts of the defineSingle() config silently did nothing:
- Hooks — hooks: { beforeRead, afterRead, beforeChange, afterChange } were
never registered, so none of them ran. They now register (via the scaffolded
init helper, alongside collection hooks) and execute on the single read and
update paths. beforeRead remains side-effect-only, matching collections.
- Field defaults — a defaultValue on a Single's field never applied; the
first read auto-created the row with null in every defaulted column, because
a function defaultValue cannot survive serialization to dynamic_singles.
Defaults are now resolved from the live code-first config, so a scalar or
structured (group/repeater) default lands on the auto-created document.
- #366 `ee20d18` Thanks @mobeenabdullah! - Singles now get their storage table on MySQL, and on any app configured with only a
DATABASE_URLrather than an explicitDB_DIALECT. The DDL for a Single's table was generated from an optional environment variable that defaults to PostgreSQL instead of from the database the statements were about to run against, and a declaredslugfield was emitted as a type MySQL cannot put a unique index on, so the table was never created and the first read reported it missing.
The plugin test harness can also boot against a real database: createTestNextly({ dialect: "postgresql" | "mysql" }) creates a dedicated database for that instance and drops it on destroy(), and getConfiguredTestDialects() reports which dialects the environment is configured for so a suite can cover those and skip the rest. The default is unchanged: in-memory SQLite.
- #381 `22b43f2` Thanks @mobeenabdullah! - Updating a Single no longer returns related fields the writer is not allowed to read.
The response expands relationships, and those rows belong to another collection carrying its own field-level access.read rules. Read paths have evaluated them since the field-access work landed; this path forwarded no caller, so it returned every related field intact — including ones the same caller's GET would withhold. That made the write path a way around the rule: write anything, read the response back.
A writer supplied a relationship id, not the related row's protected fields, so "they supplied the data" does not cover them. The rule that applies is the target collection's own, and it is now evaluated against the caller that made the write.
This reaches every hop the response expands, not only the first: a related row's own relationships carry the rules of the collection at the far end, and those are evaluated too.
Every caller the access gate applies to is judged, including one with no identity — an anonymous write permitted by a public update rule gets the same answer its read would give. Only a trusted write bypasses this, through overrideAccess rather than through an absent user.
One consequence worth knowing if you write afterUpdate hooks: they receive the response as the caller will see it, so a related field that caller may not read is already gone. That matches how reads behave — related-row rules are applied while relationships expand, before afterRead hooks run — and it is why the two paths now agree. The Single's own fields are still redacted after your hooks, unchanged.
- #369 `f822937` Thanks @mobeenabdullah! - A read that cannot assemble the evidence its access rule needs is now refused rather than allowed. This covers relationships nested in a group or repeater, and counts references as well as checking them: a
hasManyexpansion drops the entries it could not fetch, so a list that came back shorter is evidence that went missing, not evidence that nothing is there. A relationship configuredmaxDepth: 0is left alone, since an unexpanded reference is what that asks for, and so is one declared with the legacyrelationtype, which is never populated at all. Localized references are checked too, by recording what the document referred to once translations were overlaid and before anything was expanded — which is the only point a localized reference is visible at all, and makes the count of what came back comparable for those fields as well. Upload references are held to the same bar as relationships. A relationship pointing at several collections is left alone: it is stored and served as a reference rather than populated, so demanding a document there would refuse every read of a Single that has one.
Translation loading fails the read rather than reading through it. A companion query that errored was previously swallowed, leaving the main row's value in place — which a rule cannot tell apart from a translation that says so.
A relationship that exists only inside a group or repeater is now expanded on read. The check for whether a Single had any relationships to expand looked at top-level fields only, so a schema that nests all of them was returned with bare ids. Reaching into containers is opt-in, and the write-response path does not opt in: it threads no caller, so the target collection's field rules cannot be evaluated for it and the rows it pulled in could not be redacted. Expansion is best-effort by design — a related table that cannot be read yields the bare id — which is right for a response and wrong for a document about to be judged: a rule written as data.author?.suspended !== true reads the missing row as permission. Every stored reference a rule may inspect is checked to have become a row before the rule is asked, and a read whose evidence is incomplete fails instead.
A Single deleted while it is being read no longer materializes defaults nobody authorized. The rule approved the stored row; if that row disappears before the read fetches it again, what would be created is a default document no rule has seen. It is judged before it is written, rather than persisted — with its localized defaults and first version — and refused afterwards.
The depth an access rule sees no longer drops below an ordinary read's. A caller asking for depth: 0 narrows their response, and the authorization view now expands at least as far as an unqualified read would, and further when the caller asked for more.
Access callbacks can no longer write through a Map or Set in their argument, including through an object used as a Map key. They already received plain objects and arrays as copies; these were passed by reference, so a callback could change the payload it was only asked to judge. Data that refers to itself is copied without recursing forever, and a value reachable by two paths stays one object in the copy.
A ?depth=0 read still gets the references it asked for. The response deliberately leaves relationships unexpanded at that depth, so holding it to "every reference became a document" would refuse exactly what was requested; the authorization view judges those relationships at the full read depth regardless. Uploads are unaffected, since they populate at any depth.
The decision made on the document you actually receive is held to the same completeness bar as the earlier one, so expansion that succeeds before your hooks run and fails after cannot leave a rule deciding on a reference where it expects a document. That check runs on the assembled document, before your afterRead hooks shape it — a hook is free to drop or replace a relationship, and nothing tells that apart from an expansion that failed.
A read refused for incomplete evidence reports the canonical internal error rather than the underlying failure's own message, which for a database fault is schema detail. That covers relationship expansion, component population, translation loading and the per-locale overview alike.
A group or repeater whose stored value cannot be read — malformed JSON, valid JSON of the wrong shape such as a list where the field declares a group, or a repeater row that is not a row — now fails the read rather than being treated as empty, which would have walked past every relationship inside it.
Translation loading and the per-locale overview both fail the read when it is being judged, rather than leaving the fields off. An ordinary read is still served best-effort; a rule cannot tell "no translations" from "the query failed", so a read about to be judged gets the failure instead.
Metadata attached to a Map or Set under a symbol key survives the copy handed to an access callback. Arrays keep their holes and their own properties when handed to an access callback, under the keys they actually have — a decoration like "01" no longer overwrites element 1. Sparse arrays keep their holes, and a Map or Set carrying its own properties keeps them, so a rule reading either decides on the structure the payload actually has.
A subclass of Map or Set reaches an access callback as itself rather than rebuilt as the base collection, which would have discarded its methods and private state.
- #375 `0febd62` Thanks @mobeenabdullah! - Collections and Singles created in the Schema Builder can now opt out of webhook recording from their Advanced tab, so content holding personal data never reaches the outbox or any subscribed endpoint. The setting is stored on the entity, takes effect on the next write, and survives restarts. Existing installs should run
nextly migrateto add the new registry column; until then the switch has no effect and recording continues as before.
- #378 `0498e02` Thanks @mobeenabdullah! - feat(nextly): capture versions on programmatic entry writes
The tx-API and batch entry writes (createEntryInTransaction, updateEntryInTransaction, and the createEntries/updateEntries batch internals) now record a durable version snapshot and carry the full relational document (component subtrees and many-to-many relations) on their outbox event, matching the interactive create/update paths. Programmatic writers (importers, plugins, agents) previously left no version history and emitted parent-columns-only events.
- #377 `3785345` Thanks @mobeenabdullah! - Programmatic entry writes now emit webhook events. Writes through the transaction API (
createEntryInTransaction/updateEntryInTransaction), the batch helpers (createEntries/updateEntries), andpublishAllLocalespreviously recorded no webhook events, so importers, agents, and plugins writing through them were invisible to webhook subscribers. These paths now recordentry.created/entry.updatedand the correspondingpublished/unpublished/status_changedlifecycle events inside the write transaction, so an event is delivered for every entry write and is never emitted for a write that rolls back.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.43 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #368 `648c7f4` Thanks @mobeenabdullah! - The admin's design tokens now actually drive its appearance. Setting
-
--radius,--font-sansor a brand colour reaches the components that - should follow it, so a themed admin looks themed instead of only partly so.
- Radii across inputs, buttons, cards, badges and panels are derived from
-
--radiusrather than fixed per component, and the font family tokens are read - at their use sites rather than being frozen into the compiled stylesheet.
Font weights work again. font-bold, font-semibold, font-medium and
font-normal had been compiling to nothing, so headings, buttons and emphasis
rendered at the body weight throughout the admin; they now render at their
intended weight.
Several colour bugs are fixed, mostly in dark mode: sidebar navigation labels no longer take a tint from a themed brand colour, the sidebar has a distinct resting and active ink step, the email template preview frame no longer paints a white box on a dark page, and floating panels, neutral washes and the draft swatch are tinted from tokens instead of hardcoded values.
Borders are lighter. --nx-border is now a decorative separator, so tables,
cards and dividers read as quiet rules rather than hard lines, while form
controls keep a clearly visible edge: text fields, search fields, selects, the
tag, code and rich-text editors, colour pickers and the date-picker trigger are
all drawn with the control-boundary token.
Radio buttons and avatars are round again, along with switches, spinners and
status dots, which a non-zero --radius had been squaring off.
- #361 `7d5a62d` Thanks @mobeenabdullah! -
nextly migratecan be run more than once against MySQL and PostgreSQL. The core schema comparison read several dialect spellings of the same value as differences — MySQL booleans, itsnow()/CURRENT_TIMESTAMPdefaults, and PostgreSQL serial sequence defaults — so a second run reported changes to the schema the first run had just written and refused to proceed. Anextval()default over any sequence other than the one its column owns is still reported as a change.
- #306 `6481791` Thanks @faisal-rx! - Duplicate entries now report "Resource already exists." instead of the stale-version conflict message, and CLI guidance only suggests commands and flags that exist.
Creating an entry that violates a unique constraint returned 409 with "The resource has changed since you last loaded it. Please refresh and try again." — the message for an optimistic-concurrency conflict, which wrongly tells the user to refresh. The legacy service envelope now carries the canonical error code, so the REST dispatcher and the Direct API rebuild the precise DUPLICATE error.
CLI guidance is corrected to real commands: the production auto-sync guard points at nextly migrate:create + nextly migrate (previously the unregistered migrate:generate / migrate:run), nextly add no longer tells you to run the removed nextly dev, and the db:sync --force help text states the flag is a deprecated no-op. nextly upgrade and nextly migrate:resolve now accept --force-unlock, so the migrate-lock busy error's advice to re-run with that flag works on every command that takes the lock.
- #350 `ac3afca` Thanks @mobeenabdullah! - A field's declared constant
defaultValueis now applied when a collection entry is created through the REST or Direct API, not only in the admin form, and a required field carrying one can be created without supplying it. Defaults reach nested group and repeater fields too.
Two limits: a defaultValue written as a function is not applied on these paths, because the stored collection definition cannot carry a function, and bulk or caller-managed transactional creates are unchanged.
- #365 `55bc36e` Thanks @mobeenabdullah! - Code-first collections now get their tables created at boot on MySQL. The boot-time schema sync goes through an entry point that was handed a database connection rather than a connection URL, and drizzle-kit needs the MySQL database name as a separate argument, so the apply failed and the first query against the collection reported a missing table. The name now comes from the connection itself, which also fixes the publicly exported
applyDesiredSchemafor MySQL callers.
- #359 `732eb44` Thanks @mobeenabdullah! - A single polymorphic relationship (one whose
relationTolists several collections) is now recognised as a JSON-backed field on the write path, matching how upload fields with the same shape are already treated. Its value reached the driver unserialized before, so writing one could fail.
- #360 `c7a3843` Thanks @mobeenabdullah! - Retire the insecure
webhook-notificationprebuilt hook
The webhook-notification prebuilt hook (selectable in the Schema Builder's
Hooks editor) delivered over a bare fetch with no SSRF protection, and its
secret produced a base64 of the payload rather than a real HMAC. A signature
that is not an HMAC gives a false sense of authenticity, so the hook is removed
rather than left in place.
Use Nextly's signed webhook system instead: add an endpoint under Webhooks in the admin. It delivers HMAC-signed, SSRF-guarded requests through the delivery engine.
Migration: any collection that still has a stored webhook-notification hook
degrades to a no-op after upgrade (the write path skips unknown hook ids and the
admin hides the missing card), so content keeps saving. Re-create the
notification as a Webhooks endpoint to restore delivery.
- #304 `051f660` Thanks @faisal-rx! - The rich text editor now follows content-language switches and version restores.
Lexical reads its initial state once at mount, so when a localized entry or single switched language the form fetched and reset the other language's values, every regular input followed, and the editor kept displaying the first-loaded language. Stored translations were correct in the database, but the editor showed the default language for every locale, and saving from that stale screen overwrote the open locale's translation with the displayed content.
A sync plugin now loads external form-value changes into the editor: a language switch or version restore replaces the editor content, an untranslated language shows an empty document, and the editor's own keystrokes echoing back through the form are recognized and left alone so the caret never jumps while typing. The undo history is cleared on each external load so undo cannot resurrect the previous language's document into the current one.
- #354 `0c2c369` Thanks @mobeenabdullah! - Custom read rules are now enforced on Singles. A Single you restricted with one was previously readable by anyone who could reach it, because the rule was never consulted.
The rule is judged against the document you actually receive: translations resolved for the requested language, component data attached, relationships expanded. A rule reading data therefore sees the finished document rather than a partial row, which is what makes a rule such as data.secret !== true mean what it says.
That decision is made before your hooks run and before a Single is materialized on first read, so a caller your stored data refuses reaches neither. The document is assembled twice for a restricted Single: once to decide, and once for the response after your beforeRead hooks have had their turn.
The rule is then asked again about the document being returned, because a hook may have changed it. One consequence is worth knowing: if a hook is what creates the denial — it sets the very value the rule refuses — that hook has necessarily already run by the time the rule can see its effect. The earlier decision covers every refusal your stored data supports; it cannot cover one that does not exist until user code produces it.
Rules that return a query constraint are refused on Singles rather than partly applied. A constraint narrows a result set; by the time the read is decided, a Single's document has been assembled from several tables and no longer corresponds to one row for the database to test the predicate against. Return a boolean from a Single's read rule; constraints continue to work on collections, where they are folded into the query.
A rule that returns no decision at all now denies, on collections as well as Singles. A rule is free to fall through without returning, and such a result was previously read as "allowed, with nothing to filter by" — admitting the caller and narrowing nothing.
Field-level read access is applied to a Single after the read is decided, not before. A field your rule inspects is no longer removed from the document the rule is shown, so a rule guarding a value the caller may not read decides on that value rather than on its absence.
Ownership is always decided against the stored row, and against the row actually being returned. An owner-only Single is not judged on the response object, which an afterRead hook or a field read rule is free to strip the owner identifier from — a transformation that could refuse a document to its real owner. It is judged on the row read before your hooks and again on the row read after them, so a hook write or a concurrent owner change cannot hand back a document the caller no longer owns.
A first read of a Single that has never been written is judged against the defaults it would create, so a rule that refuses those defaults no longer lets the read materialize the document (and its first version) before returning 403.
Your own claims on a user now reach the access rules, on every transport. A custom rule reading a tenant, a plan or an entitlement saw undefined, because the caller was rebuilt from a fixed list of canonical fields at four separate layers: the Direct API namespaces, the collection access service, the Single access gate, and the REST route-auth boundary. A rule written to refuse a caller therefore admitted it. Custom JWT claims are now carried from the verified session through to the rule, and the Direct API's UserContext accepts them explicitly, along with roles for rules that decide on more than one. A claim can never displace the authenticated identity: id and roles come from what the route authenticated, not from what the token says about itself.
A read rule whose exclusion list comes back empty no longer denies everyone. { id: { not_in: [] } } excludes nothing, so it restricts nothing — but it translated to no SQL condition, and a constraint that narrows nothing is refused rather than allowed to widen a read. Members that cannot narrow anything are now removed before that judgement, so the rest of the rule is what decides, and a rule made up entirely of them permits the read. An empty in list is still refused: it should match nothing, and honouring it after translation dropped it would widen the read to every row.
Relationship depth no longer changes who is allowed to read. ?depth=0 shapes the response, and letting it shape the authorization view too gave a caller a way to blind a rule: the relationship stayed an id, so a rule reading into the related row saw nothing and read that as permission. Authorization uses the full read depth whatever the caller asked for.
Field-level access.read callbacks are handed a detached copy too, and so are field write callbacks. They run after the document-level decision, so a callback that reached into a shared group, repeater or component could change a document that had already been authorized — with nothing to judge it again. The copy is taken before nested fields are redacted, so a rule at the parent level still sees what the document held when the pass began rather than what an earlier-registered field's redaction left behind. Values that cannot be structurally cloned — a JSON prop defining toJSON(), for instance — are passed through rather than rejected, so isolating the snapshot never fails a valid write.
findSingle and findSingles forward your fallbackLocale. It was dropped, so a no-fallback read still fell back to the default language through the Direct API, and a rule keyed on it saw undefined.
An access rule that writes to its data argument no longer changes the response. Rules are handed a detached deep copy, so a rule remains a decision rather than a transformation — a shallow one still shared every component, repeater and expanded relation with the response — and password values are stripped after every callback that could reintroduce one.
A rule that reads an expanded relationship now sees the related row as stored, not as the response will show it. Related rows are redacted against the target collection's own field rules, and doing that before the decision handed the rule the hole rather than the value, so data.author?.suspended !== true read undefined and admitted a caller the stored data refuses. The response is still redacted; only the decision sees through it.
A draft Single stays hidden from an untrusted caller even when a stored rule would refuse them. The rule was decided before the draft/published filter, so the answer was 403 rather than the 404 that conceals a draft — which disclosed both that the row exists and what the rule made of the caller.
- #371 `b8bf6d4` Thanks @mobeenabdullah! - Media cards keep their metadata inside the tile, four more surfaces stay within their rounded corners, and the corner-radius guide now matches the components.
In the media library's grid view, a card's file size could paint outside the card border while its dimensions were squeezed to nothing. At the six-column layout this left the dimensions reading as a stray "1..", and a long label such as "Invalid size" spilled past the tile edge. The size is now always readable inside the card, and the dimensions appear once the card is wide enough to render both values in full, with a tooltip on the row carrying both at any card width.
Four surfaces painted a full-bleed child square across a rounded parent, which anyone running a nonzero --radius could see: the email-template segmented control, the component-row card header, the schema-builder field table header, and the code editor's validation error strip. CardHeader also gained the top-corner counterpart of the fix CardFooter already carried. All of these are unchanged at the shipped --radius: 0.
The slash command menu in rich text fields declared a stacking order that never took effect, so it could be covered by a dialog. It now sits above one.
The corner-radius tier tables in the theme and in the plugin authoring guide described a system the components do not implement, pointing plugin authors at the wrong step for alerts, table wrappers, checkboxes, icon buttons, switches and tabs. Both now agree with the code and with each other, they no longer offer rounded-xl and rounded-2xl as steps of the radius knob (the published Tailwind preset never exported them, and they do not go square at --radius: 0), and they state what --radius: 0 actually resolves to for each step. A new test pins the contract so the documents and the components cannot drift apart unnoticed.
- #363 `8de5ea3` Thanks @mobeenabdullah! - Content-route reads now enforce publish state and access, and localized draft translations no longer leak.
resolveContent and createContentRoute (from nextly/runtime) now default to reading only status: "published" through the lifecycle-aware publish filter, so for a localized collection a draft translation under a published main row is no longer returned. They also enforce the collection's read-access rules by default: a rule-less (public) collection still renders, but a collection with a stored member-only or role-based read rule is hidden from an unauthenticated request (it resolves to notFound()). Pass a user to render member content, or overrideAccess: true for a fully trusted read.
The Direct API find gains a status?: "published" | "draft" | "all" option that drives the same lifecycle-aware filter (constraining a localized collection's per-locale companion status), replacing the previous statusField where-clause on the content-route helpers. Status-less collections are handled automatically — the scope is a no-op there.
- #355 `521e453` Thanks @mobeenabdullah! - nextly now ships content routing and sitemap/robots delivery from
nextly/runtime:resolveContent(F1-cached published-by-slug lookup that rethrows on a transient error),createContentRoute(an optional catch-all factory that resolves any path to a published entry, withgenerateStaticParams,generateMetadata, and a reserved-path denylist),isReservedPath, andnextlySitemap/nextlyRobotsfor the canonicalapp/sitemap.tsandapp/robots.ts.cachedFindnow runs the read UNCACHED (instead of throwing a framework invariant) when called outside a Next request/build scope, so content reads work in tests, scripts, and other non-request contexts.
- #362 `0da91f5` Thanks @mobeenabdullah! - Add the
nextly webhooks:prunecommand
nextly webhooks:prune runs a webhook-queue retention pass on demand (with
--dry-run), so a self-hosted install can reclaim the fanned-out event ledger
and terminal delivery log from a cron job. It reads the same webhooks.retention
policy as the automatic passes and does nothing when retention is disabled. See
the new "Webhook queue retention & VACUUM" guide.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 17 packages at 0.0.2-alpha.42 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
- #332 `80febb5` Thanks @mobeenabdullah! - Block props now go through the field system: a block declares its editable props with the same field types a collection uses, and their values are validated by the same server-side pass entries get. Binding a data field to a block prop is derived from the prop type, so every compatible prop offers it without the block opting in.
- #343 `a614d3d` Thanks @mobeenabdullah! - Collections and singles can now hold a page built from blocks. Add a field with
blocks({ name: "content" }), optionally naming which registered blocks it accepts, and the whole page document is stored in one column and typed for you when you generate types.
- #346 `4f39297` Thanks @mobeenabdullah! - Field-level read rules now reach related rows inside components. A component's relationship fields copy whole rows out of the collection they point at, and neither the parent entity's field list nor the component's describes that collection's fields, so a field you protected there was returned inside the populated component to any caller that could read the parent. Reading a collection entry or a Single now judges those related rows by the rules of the collection they come from, for the caller making the request.
This completes the read side of the redaction added for direct relationships. Write-side callers that assemble a payload without a caller are unchanged, so a mutation response still returns what it did before.
- #347 `81204af` Thanks @mobeenabdullah! - Outbox event recording is now endpoint-gated. A content write records a webhook event only when the install has at least one enabled webhook endpoint, or when the new
webhooks.auditoption is turned on. Installs with no webhooks configured no longer pay an event-table insert and a full-document serialization on every write.
Recording resumes immediately when an endpoint is created in the same process, and within about 30 seconds for one created in another process. A few events may still be recorded just after the last endpoint is removed; retention prunes them.
- #344 `2dec172` Thanks @mobeenabdullah! - The scaffolded blog template now uses tag-based ISR: publishing or editing content in the admin refreshes the affected pages on the next request, with no rebuild and no 60-second timer. Content-template scaffolds (blog) also install
nextlyand@nextlyhq/*from thealphadist-tag so they always get thenextly/runtimecache helpers the pages use.
- #339 `9ab5f19` Thanks @mobeenabdullah! - Content pages can now use tag-based ISR: cache a read with
cachedFindand tag it withnextlyTagsfromnextly/runtime, and every content change (create, update, publish, unpublish, delete, or slug rename) busts exactly those tags so the page regenerates on the next visit — no rebuild, noforce-dynamic. Revalidation turns on automatically wherever you mount the admin route (createDynamicHandlers). A per-operationdisableRevalidateflag lets a bulk import, seed, or CLI write skip it. See the new "ISR and caching" guide, including the rule for keying a per-user read so it cannot leak across callers.
- #337 `6f19e60` Thanks @mobeenabdullah! - Collections and singles created in the Schema Builder now carry their cache-revalidation setting. A new "Cache revalidation" switch on the Advanced tab (on by default) lets you opt a collection or single out of busting cache tags on write, and the setting round-trips through boot, HMR,
db:sync, andmigrate:createthe same way code-firstrevalidateconfig does. Existing databases pick up the new registry column when you runnextly migrate(boot warns until it is run).
- #336 `d0a45d5` Thanks @mobeenabdullah! - Collections and singles can now opt out of webhook recording with
webhooks: false(or{ record: false }). Form submissions opt out by default, so visitor IP address, user agent, and submission content are no longer recorded to the webhook outbox or delivered to endpoints subscribed toentry.createdor*. Existing installs: submission events recorded before this release remain in the outbox and can be pruned manually; no data is deleted automatically.
- #351 `b44b1a3` Thanks @mobeenabdullah! - @nextlyhq/plugin-seo now generates a sitemap of your published content and serves it at a public HTTP route under Nextly's dynamic handler (in a scaffolded app,
/admin/api/plugins/@nextlyhq/plugin-seo/sitemap.xml). It lists one URL per published entry across the collections you configure, reflects publishes and edits on the next request, and leaves out drafts and any page markednoindex. Configure the site origin withbaseUrland per-entry paths withurlFor, or disable the route withsitemap: false.
- #348 `c0b3796` Thanks @mobeenabdullah! - Read rules that narrow by a filter are now applied in full. A stored read rule can return a filter describing which rows the caller may see, and only part of it was being applied: the first field's
equalsvalue. A rule naming two fields filtered by one of them, a rule using any other operator applied nothing at all, and a rule whose value was legitimately falsy —0,false, an empty string — also applied nothing. In each of those cases the read returned rows the rule was written to exclude, and the matching count reported them too.
Filters now go through the same translation your own where clauses use, so every field and every supported operator binds. Owner-only rules are unaffected: a single non-empty owner id was the one shape the old path handled correctly, which is why this went unnoticed.
A filter is applied only if all of it can be applied, and access filters are held to a narrower shape than the where clauses you write yourself. A filter may name columns on the collection (or its localized fields) and compare them with any supported operator, including the shorthand { field: value } form. Logical and/or groups, dotted paths like author.name, and empty in/not_in lists are refused rather than approximated, because each of those translates to something narrower than the rule states — or, in the dotted case, to a comparison against a different column.
A refused filter is reported as forbidden, and the matching count refuses identically. If you need a shape that is currently refused, the read fails closed instead of quietly returning more than the rule allows.
- #335 `8512d5d` Thanks @mobeenabdullah! - Field-level read rules now apply to related rows. Populating a relationship copies the whole related row into the parent entry, and a field's
access.readwas only ever evaluated against the collection being read, never against the collection on the other end of the relationship. A field you protected on one collection was therefore returned in full to anyone who reached it through a relationship from another, at any depth. Passwords and system columns were already stripped there; this closes the same gap for the rules you write yourself.
Each related row is now judged by its own collection's rules, for the caller making the request, so a relationship cannot return more than a direct read of that row would. Trusted server-side reads that pass overrideAccess are unaffected, and secrets are still stripped for every caller regardless.
If you relied on reading a protected field indirectly through a relationship, that field will now be absent: read it as the collection that owns it, with a caller its rule admits.
- #333 `ba3e8f4` Thanks @mobeenabdullah! - Collection read rules now apply over the REST API. Listing, fetching and counting entries previously ignored who was asking, so a collection configured with an owner-only or role-based read rule still returned every row to any caller who could reach the endpoint — the rule only ever held on writes and inside the Direct API. Reads now evaluate the caller against the collection's stored rules, with owner-only scoping applied in the database query so pagination and totals stay correct, and a count can no longer describe rows the caller is not allowed to see.
Role-based read rules are evaluated against the caller's resolved roles, and a super-admin keeps the bypass they already have everywhere else. A scoped API key is judged on its own read grant rather than on the permissions of the account that issued it, so a read-only key issued by an administrator is no longer treated as that administrator's full session.
If you configured a read rule expecting it to be enforced, this closes that gap. If instead something in your app depended on reads returning unfiltered data, it will now see only the rows its rule allows: check any integration that reads with a user session or API key against a collection whose read rule is not public.
- #356 `a24c17e` Thanks @mobeenabdullah! - REST reads now default to published-only. A list, get, or count request to a Draft/Published collection or single with no
?status=returns only published entries; pass?status=allor?status=draftto include drafts (subject to your read access rules). Previously these reads defaulted to returning every status, which could expose drafts to any caller.
An invalid ?status= value (for example a typo like ?status=pubished) is now rejected with a 400 instead of being silently treated as "all", so a malformed filter can never widen a read. Trusted server-side Direct API calls are unchanged (they still see every status). The admin panel already requests every status, so editors continue to see their drafts.
- #338 `1687ff1` Thanks @mobeenabdullah! - Read rules on Singles now apply over the REST API. A Single's stored read rule was enforced on every update and inside the Direct API, but reading the document over HTTP skipped it entirely, so a Single you restricted to a role was still returned in full to any caller who could reach the endpoint. Reads now evaluate the caller against the rule you configured, and a Single's related rows are redacted by the field rules of the collection they come from. Relationships reached through an embedded component are not yet covered.
A scoped API key is judged on its own read grant rather than on the permissions of the account that issued it, and super-admins keep the bypass they have everywhere else.
An owner-only read is judged against the document itself, since a Single has no list query to fold an ownership filter into.
custom read rules on Singles are not enforced by this change. A custom function may return a query constraint, which a list read compiles into SQL; applying that to a single document would mean re-implementing the filter grammar, so it is left as it behaves today rather than partly applied. public, authenticated, role-based and owner-only read rules are all enforced. That rule reports "allowed" for any authenticated caller and hands back the predicate a list query would have filtered by, which a Single has no list to apply, so the predicate is checked against the row instead.
The standalone nextly/api/singles-detail GET route is deliberately public and does not authenticate. A Single with no read rule stays publicly readable there, exactly as before. A Single you restrict is no longer served by that route at all, including to callers the rule would admit, because the route has no caller to evaluate. Read restricted Singles through the authenticated API instead.
If you configured a read rule on a Single expecting it to be enforced, this closes that gap. If something in your app read a restricted Single over HTTP and depended on getting it, that call will now be denied: give the caller a role the rule admits, or read it through the Direct API, which is trusted by default.
- #353 `48f82a8` Thanks @mobeenabdullah! - nextly now exports
buildMetadatafromnextly/runtime: it maps a content entry's SEO field group (from@nextlyhq/plugin-seo) to a Next.jsMetadataobject, so a page'sgenerateMetadatabecomes a single call instead of a hand-written mapping. It sets the title, description, canonical, OpenGraph, Twitter card, robots (fromnoindex), and hreflang alternates, with per-call fallbacks for blank fields. Thenextdependency is type-only, so importing it never forcesnextat load.
- #349 `9cab18c` Thanks @mobeenabdullah! - Add the first-party @nextlyhq/plugin-seo package. Register it in your config to add an SEO field group (title, description, OG image, canonical, noindex) to the collections you name. It is opt-in and framework-agnostic (no Next.js dependency), so it is safe in headless and admin-only projects.
The plugin SDK now also re-exports the field-authoring factories (text, textarea, checkbox, upload, group) and the FieldConfig type, so plugin authors get the whole authoring surface from @nextlyhq/plugin-sdk.
- #340 `3d48019` Thanks @mobeenabdullah! - Fixed a dev-mode gap where setting a collection or single to
webhooks: falsedid not take effect if the same config reload also hit a schema error (for example a transient database blip during introspection, or a change awaiting confirmation). The recording opt-out is now applied up front, so a newly private entity stops recording immediately even when the rest of the reload is deferred; re-enabling recording still waits for a clean schema sync.
- #341 `a98cdcf` Thanks @mobeenabdullah! - Fixed webhook-outbox retention not running after a Single update that opts out of both recording (
webhooks: false) and cache revalidation (revalidate: { disable: true }) when a post-commit hook then fails. The Single write result now carries an explicit committed-write signal, matching the collection path, so the write-path cleanup runs for every durable write on installs without a scheduled webhook drain.
- #342 `f0b4fc3` Thanks @mobeenabdullah! - A Single that opts out of webhook recording (
webhooks: false) no longer assembles its webhook payload on update. Previously the previous/next event documents were built (reading every component subtree) before the opt-out was checked, so a scalar update to an opted-out Single still performed webhook-only component reads and could fail on a missing or stale component table. The opt-out is now resolved before any payload assembly.
- #345 `38d50d0` Thanks @mobeenabdullah! - Collection status webhook events now fire. Publishing an entry delivers
entry.published(and the genericentry.status_changed); unpublishing deliversentry.unpublished(andentry.status_changed); any other status change deliversentry.status_changed. A create-as-published deliversentry.created+entry.published. Per-locale status changes on a localized collection are tagged with their locale. Every status event carries an explicitstatusChange: { from, to }. Only Draft/Published collections emit these, and collections that opt out of recording (webhooks: false) emit none. Previously these event types were subscribable in the admin UI but never fired.
Packages
@nextlyhq/adapter-drizzle@nextlyhq/adapter-mysql@nextlyhq/adapter-postgres@nextlyhq/adapter-sqlite@nextlyhq/admin@nextlyhq/admin-css@nextlyhq/blocks-engine@nextlyhq/plugin-form-builder@nextlyhq/plugin-page-builder@nextlyhq/plugin-sdk@nextlyhq/plugin-seo@nextlyhq/storage-s3@nextlyhq/storage-uploadthing@nextlyhq/storage-vercel-blob@nextlyhq/uicreate-nextly-appnextly
Released 16 packages at 0.0.2-alpha.41 in lockstep.
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### @nextlyhq/adapter-mysql
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.41
- ### @nextlyhq/adapter-postgres
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.41
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.41
- ### @nextlyhq/admin
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - nextly@0.0.2-alpha.41
- - @nextlyhq/ui@0.0.2-alpha.41
- ### @nextlyhq/admin-css
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### @nextlyhq/blocks-engine
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### @nextlyhq/plugin-form-builder
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - @nextlyhq/admin@0.0.2-alpha.41
- - nextly@0.0.2-alpha.41
- - @nextlyhq/plugin-sdk@0.0.2-alpha.41
- - @nextlyhq/ui@0.0.2-alpha.41
- ### @nextlyhq/plugin-page-builder
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - @nextlyhq/admin@0.0.2-alpha.41
- - nextly@0.0.2-alpha.41
- - @nextlyhq/plugin-sdk@0.0.2-alpha.41
- - @nextlyhq/ui@0.0.2-alpha.41
- ### @nextlyhq/plugin-sdk
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - @nextlyhq/admin@0.0.2-alpha.41
- - nextly@0.0.2-alpha.41
- ### @nextlyhq/storage-s3
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### @nextlyhq/ui
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### create-nextly-app
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport. - ### nextly
Patch Changes
- #328 `a4f503d` Thanks @mobeenabdullah! -
@nextlyhq/blocks-enginenow providesdefineBlockfor declaring a block type — its props, default styles, child slots, style capabilities, and how it renders — plus the registry that collects them when an app boots. Mistakes are caught at startup with a clear message instead of surfacing as broken pages: a duplicate block name names both sources, and bumping a block's version without providing the matching upgrade step is refused outright. Third parties can add new style capabilities throughregisterSupport.
- Updated dependencies [`a4f503d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.41
- - @nextlyhq/adapter-mysql@0.0.2-alpha.41
- - @nextlyhq/adapter-postgres@0.0.2-alpha.41
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.41
Released all 12 packages at 0.0.2-alpha.30 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/plugin-sdk
Patch Changes
- #145 `76bde2a` Thanks @muzzamil-rx! - The API reference was not correctly specified in the
useEffectdependency array. It was set as[api], whereas it should have been[api.public].
- Updated dependencies [`76bde2a`]:
- - @nextlyhq/admin@0.0.2-alpha.30
- - nextly@0.0.2-alpha.30
Released all 12 packages at 0.0.2-alpha.28 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-mysql
Patch Changes
- Updated dependencies []:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.28
- ### @nextlyhq/adapter-postgres
Patch Changes
- Updated dependencies []:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.28
- ### @nextlyhq/adapter-sqlite
Patch Changes
- Updated dependencies []:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.28
- ### @nextlyhq/admin
Patch Changes
- #134 `0363799` Thanks @faisal-rx! - Remove the hardcoded default super-admin credentials from
seedSuperAdmin(). The seeder no longer falls back to a built-in email/password pair: callers (the/admin/setupwizard and the dev seed) must pass an explicitemailandpassword, and the function throws aVALIDATION_ERRORif either is missing.seedAll()likewise fails closed when super-admin seeding is enabled but no credentials are supplied, instead of creating a known-weak default account. This removes a well-known default credential from shipped framework source.
Also hides the placeholder address the admin user menu previously showed when a user had no email (the line is now omitted when empty), and standardizes example email placeholders across the admin and form-builder UIs onto the nextly.local domain.
- Updated dependencies []:
- - @nextlyhq/ui@0.0.2-alpha.28
- ### nextly
Patch Changes
- #134 `0363799` Thanks @faisal-rx! - Remove the hardcoded default super-admin credentials from
seedSuperAdmin(). The seeder no longer falls back to a built-in email/password pair: callers (the/admin/setupwizard and the dev seed) must pass an explicitemailandpassword, and the function throws aVALIDATION_ERRORif either is missing.seedAll()likewise fails closed when super-admin seeding is enabled but no credentials are supplied, instead of creating a known-weak default account. This removes a well-known default credential from shipped framework source.
Also hides the placeholder address the admin user menu previously showed when a user had no email (the line is now omitted when empty), and standardizes example email placeholders across the admin and form-builder UIs onto the nextly.local domain.
- Updated dependencies []:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.28
- - @nextlyhq/adapter-postgres@0.0.2-alpha.28
- - @nextlyhq/adapter-mysql@0.0.2-alpha.28
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.28
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #134 `0363799` Thanks @faisal-rx! - Remove the hardcoded default super-admin credentials from
seedSuperAdmin(). The seeder no longer falls back to a built-in email/password pair: callers (the/admin/setupwizard and the dev seed) must pass an explicitemailandpassword, and the function throws aVALIDATION_ERRORif either is missing.seedAll()likewise fails closed when super-admin seeding is enabled but no credentials are supplied, instead of creating a known-weak default account. This removes a well-known default credential from shipped framework source.
Also hides the placeholder address the admin user menu previously showed when a user had no email (the line is now omitted when empty), and standardizes example email placeholders across the admin and form-builder UIs onto the nextly.local domain.
- Updated dependencies [`0363799`]:
- - nextly@0.0.2-alpha.28
- - @nextlyhq/admin@0.0.2-alpha.28
- - @nextlyhq/ui@0.0.2-alpha.28
Released all 12 packages at 0.0.2-alpha.26 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
### @nextlyhq/adapter-mysql
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
- Updated dependencies [`6964718`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.26
- ### @nextlyhq/adapter-postgres
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
- Updated dependencies [`6964718`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.26
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
- Updated dependencies [`6964718`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.26
- ### @nextlyhq/admin
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
- Updated dependencies [`6964718`]:
- - @nextlyhq/ui@0.0.2-alpha.26
- ### create-nextly-app
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
### nextly
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
- Updated dependencies [`6964718`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.26
- - @nextlyhq/adapter-mysql@0.0.2-alpha.26
- - @nextlyhq/adapter-postgres@0.0.2-alpha.26
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.26
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
- Updated dependencies [`6964718`]:
- - @nextlyhq/admin@0.0.2-alpha.26
- - nextly@0.0.2-alpha.26
- - @nextlyhq/ui@0.0.2-alpha.26
- ### @nextlyhq/storage-s3
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
### @nextlyhq/storage-uploadthing
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
### @nextlyhq/ui
Patch Changes
- #123 `6964718` Thanks @aqib-rx! - Single edit forms no longer ask for a title and slug. A Single is a one-instance document whose identity is fixed by its config (
label+slug), but the admin previously rendered title and slug as editable, required inputs — forcing redundant input for values already determined by the definition.
The single edit form now shows the title (from the single's label) and slug (from the configured slug) as read-only, non-editable fields, and submitting never errors on them. EntrySystemHeader and EntryMetaStrip gain opt-in lockIdentity/lockSlug flags (default off, so collection entry forms are unchanged); for singles the title/slug are seeded from config, the client validation for those two fields is relaxed, and slug auto-generation is disabled.
Released all 12 packages at 0.0.2-alpha.25 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
### @nextlyhq/adapter-mysql
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
- Updated dependencies [`8cc3a1c`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.25
- ### @nextlyhq/adapter-postgres
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
- Updated dependencies [`8cc3a1c`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.25
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
- Updated dependencies [`8cc3a1c`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.25
- ### @nextlyhq/admin
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
- Updated dependencies [`8cc3a1c`]:
- - @nextlyhq/ui@0.0.2-alpha.25
- ### create-nextly-app
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
### nextly
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
- Updated dependencies [`8cc3a1c`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.25
- - @nextlyhq/adapter-mysql@0.0.2-alpha.25
- - @nextlyhq/adapter-postgres@0.0.2-alpha.25
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.25
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
- Updated dependencies [`8cc3a1c`]:
- - @nextlyhq/admin@0.0.2-alpha.25
- - nextly@0.0.2-alpha.25
- - @nextlyhq/ui@0.0.2-alpha.25
- ### @nextlyhq/storage-s3
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
### @nextlyhq/storage-uploadthing
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
### @nextlyhq/ui
Patch Changes
- #121 `8cc3a1c` Thanks @aqib-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer fail to install under pnpm 11. pnpm 11 stopped reading thepnpmfield frompackage.json, so thepnpm.onlyBuiltDependenciesallowlist the scaffolder emitted was ignored:pnpm installaborted withERR_PNPM_IGNORED_BUILDS, and past thatbetter-sqlite3never compiled its native binding (SQLite scaffolds crashed at boot) whilesharp,esbuild, andunrs-resolverwere silently blocked.
The scaffolder now writes the build-script allowlist to pnpm-workspace.yaml instead, emitting both allowBuilds (read by pnpm 11+) and onlyBuiltDependencies (read by pnpm 10.6+), and drops the now-dead pnpm field from the generated package.json. better-sqlite3 is always allow-listed so the --use-yalc dev flow — which installs every adapter — builds it too. npm, yarn, and pnpm 9 run dependency build scripts by default and ignore the file, so it is harmless under those package managers.
Released all 12 packages at 0.0.2-alpha.24 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
### @nextlyhq/adapter-mysql
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
- Updated dependencies [`01f3f7a`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.24
- ### @nextlyhq/adapter-postgres
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
- Updated dependencies [`01f3f7a`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.24
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
- Updated dependencies [`01f3f7a`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.24
- ### @nextlyhq/admin
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
- Updated dependencies [`01f3f7a`]:
- - @nextlyhq/ui@0.0.2-alpha.24
- ### create-nextly-app
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
### nextly
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
- Updated dependencies [`01f3f7a`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.24
- - @nextlyhq/adapter-mysql@0.0.2-alpha.24
- - @nextlyhq/adapter-postgres@0.0.2-alpha.24
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.24
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
- Updated dependencies [`01f3f7a`]:
- - @nextlyhq/admin@0.0.2-alpha.24
- - nextly@0.0.2-alpha.24
- - @nextlyhq/ui@0.0.2-alpha.24
- ### @nextlyhq/storage-s3
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
### @nextlyhq/storage-uploadthing
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
### @nextlyhq/ui
Patch Changes
- #103 `01f3f7a` Thanks @faisal-rx! - Forward
cc/bccconsistently across every email send path.
nextly.email.send and nextly.email.sendWithTemplate (Direct API) now accept and forward cc/bcc — they are added to SendEmailArgs and SendTemplateEmailArgs. Previously the Direct API namespace silently dropped both fields, so only the REST route (/api/email/send-with-template) honored them. EmailService.sendWithTemplate also dropped cc/bcc on its code-first template fallback branch while the DB-template branch already forwarded them; both branches now forward them. Empty cc/bcc arrays are not forwarded, so they don't override the "no options" path.
Released all 12 packages at 0.0.2-alpha.23 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
### @nextlyhq/adapter-mysql
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
- Updated dependencies [`7f7845b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.23
- ### @nextlyhq/adapter-postgres
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
- Updated dependencies [`7f7845b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.23
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
- Updated dependencies [`7f7845b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.23
- ### @nextlyhq/admin
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
- Updated dependencies [`7f7845b`]:
- - @nextlyhq/ui@0.0.2-alpha.23
- ### create-nextly-app
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
### nextly
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
- Updated dependencies [`7f7845b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.23
- - @nextlyhq/adapter-mysql@0.0.2-alpha.23
- - @nextlyhq/adapter-postgres@0.0.2-alpha.23
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.23
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
- Updated dependencies [`7f7845b`]:
- - @nextlyhq/admin@0.0.2-alpha.23
- - nextly@0.0.2-alpha.23
- - @nextlyhq/ui@0.0.2-alpha.23
- ### @nextlyhq/storage-s3
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
### @nextlyhq/storage-uploadthing
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
### @nextlyhq/ui
Patch Changes
- #101 `7f7845b` Thanks @faisal-rx! - Fix component CRUD breaking with a 500 after a dev-server config hot-reload.
reloadNextlyConfig rebuilt the runtime Drizzle descriptors for comp_* data tables with the collection/single generateRuntimeSchema, which prepends id/title/slug base columns and omits the _parent_id/_parent_table/_parent_field/_order link columns that components use to reference their parent document. This overwrote the correct boot-time registration.
After a hot-reload the bad descriptor no longer matched the physical table, so component reads (which filter by _parent_id) failed and were swallowed as "no rows", and component writes (which insert the _parent_* columns) were rejected by the database. Saving any Single or Collection document that embeds a component returned a 500.
The reload path now builds comp_* descriptors with ComponentSchemaService.generateRuntimeSchema, matching the boot path and the physical comp_* table. Adds a regression test asserting the refreshed descriptor exposes the _parent_* link columns and not title/slug.
Released all 12 packages at 0.0.2-alpha.22 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success. - ### @nextlyhq/adapter-mysql
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success.
- Updated dependencies [`bdece5c`, `faf14cd`, `17f0353`, `7f465db`, `7cae340`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.22
- ### @nextlyhq/adapter-postgres
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success.
- Updated dependencies [`bdece5c`, `faf14cd`, `17f0353`, `7f465db`, `7cae340`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.22
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success.
- Updated dependencies [`bdece5c`, `faf14cd`, `17f0353`, `7f465db`, `7cae340`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.22
- ### @nextlyhq/admin
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success.
- Updated dependencies [`bdece5c`, `faf14cd`, `17f0353`, `7f465db`, `7cae340`]:
- - @nextlyhq/ui@0.0.2-alpha.22
- ### create-nextly-app
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success. - ### nextly
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success.
- Updated dependencies [`bdece5c`, `faf14cd`, `17f0353`, `7f465db`, `7cae340`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.22
- - @nextlyhq/adapter-mysql@0.0.2-alpha.22
- - @nextlyhq/adapter-postgres@0.0.2-alpha.22
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.22
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success.
- Updated dependencies [`bdece5c`, `faf14cd`, `17f0353`, `7f465db`, `7cae340`]:
- - @nextlyhq/admin@0.0.2-alpha.22
- - nextly@0.0.2-alpha.22
- - @nextlyhq/ui@0.0.2-alpha.22
- ### @nextlyhq/storage-s3
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success. - ### @nextlyhq/ui
Patch Changes
- #87 `bdece5c` Thanks @faisal-rx! - Fix code-first / HMR schema applies wrongly dropping managed tables on SQLite & MySQL.
On SQLite and MySQL, drizzle-kit's pushSchema ignores tablesFilter and introspects the whole database, so any managed table missing from the desired schema was flagged as a data-losing "orphan" DROP — failing the apply and offering the table as a spurious rename source. Three cases are fixed:
- Schema-events ledger (nextly_schema_events) is now a first-class managed core table (declared in getCoreSchema / getDialectTables / CORE_TABLE_NAMES), so no schema path — apply, HMR, migrate, or db:sync — ever treats it as an orphan drop or offers it as a spurious rename target. To make it round-trip cleanly, the SQLite primary key gains an explicit NOT NULL (SQLite, unlike PG/MySQL, treats a bare TEXT PRIMARY KEY as nullable) and the SQLite partial unique index is dropped — drizzle-kit 0.31.10 cannot round-trip a SQLite partial index (drizzle-team/drizzle-orm#4688), and keeping it churned DROP/CREATE INDEX on every push. Postgres keeps its partial unique index. The "one applied row per file" guarantee is now enforced in code on all dialects: an atomic conditional markApplied (sets applied only when no other applied row exists for the filename) plus the existing cross-process migrate lock.
- UI-created collections, singles, and components are now preserved during a code-first HMR apply: every DB-registered resource is included in the desired schema (code-config entries take precedence), so adding a collection in code no longer drops resources created via the admin UI.
- Migration status: a collection added in code after the initial DB setup is now marked applied once its table is created, instead of showing pending forever in the builder listing (mirrors the existing singles behaviour).
- #87 `faf14cd` Thanks @faisal-rx! - Fix fresh-database first-run aborting on MySQL.
Now that nextly_schema_events is a core table, freshPushSchema creates it (and its indexes) during first-run setup. The setup then also replayed the out-of-band getSchemaEventsDdl unconditionally, and the MySQL raw DDL's CREATE INDEX has no IF NOT EXISTS, so it failed with a duplicate-index error and first-run reported failure on a fresh MySQL database. The out-of-band bootstrap is now guarded by a tableExists check (matching nextly migrate's ensureLedger), so it only runs as a fallback when the ledger is genuinely missing.
- #87 `17f0353` Thanks @faisal-rx! - Fix
nextly migrate:creategenerating the wrong schema for components.
The migration snapshot generator built component tables with the collection table-builder, so they came out with slug/title and were missing the component embedding columns (_parent_id, _parent_table, _parent_field, _order, _component_type). The generated snapshot then diverged from the real component table the apply pipeline creates, which made nextly migrate:resolve --applied fail its schema-match verification for any project with a component. Components now use buildDesiredTableFromComponentFields, matching the apply path.
- #87 `7f465db` Thanks @faisal-rx! - Fix
nextly migrate:createomitting the component parent index, which brokemigrate:resolve --applied.
The apply pipeline always creates a composite index (idx_<table>_parent on _parent_id, _parent_table, _parent_field) for component tables, but the migration-snapshot builder did not emit it. So the live index looked like an unmanaged extra and nextly migrate:resolve --applied failed verification ("Live schema does not match the target snapshot") for any project with a component. The snapshot builder now emits the parent index, matching the apply pipeline.
- #87 `7cae340` Thanks @faisal-rx! - Fix two
nextly_schema_eventsledger edge cases on the code-first schema path. - - Postgres index/default churn: the ledger's raw bootstrap DDL declared
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), but the Drizzle def supplies the value app-side ($defaultFn) with no SQL default. Now that the ledger is a core table flowing through drizzle-kit's Postgres diff, that mismatch made every push/migrate emitALTER COLUMN started_at DROP DEFAULT. The raw DDL now omits the redundant default (matching the MySQL/SQLite ledger DDL and theidcolumn), so the ledger round-trips cleanly with no churn. Added a Postgres round-trip integration test alongside the existing SQLite one. - -
markAppliedrace no-op: when the "one applied row per file" guard blocked a concurrent second apply, the losing row was left dangling atin_progressand the caller still logged a success.markAppliednow resolves the blocked row tosupersededand returns whether it applied, andnextly migratereports the file as already-applied-by-a-concurrent-run instead of a false success.
Released all 12 packages at 0.0.2-alpha.21 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list. - ### @nextlyhq/adapter-mysql
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list.
- Updated dependencies [`0e17fc6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.21
- ### @nextlyhq/adapter-postgres
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list.
- Updated dependencies [`0e17fc6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.21
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list.
- Updated dependencies [`0e17fc6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.21
- ### @nextlyhq/admin
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list.
- Updated dependencies [`0e17fc6`]:
- - @nextlyhq/ui@0.0.2-alpha.21
- ### create-nextly-app
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list. - ### nextly
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list.
- Updated dependencies [`0e17fc6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.21
- - @nextlyhq/adapter-mysql@0.0.2-alpha.21
- - @nextlyhq/adapter-postgres@0.0.2-alpha.21
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.21
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list.
- Updated dependencies [`0e17fc6`]:
- - @nextlyhq/admin@0.0.2-alpha.21
- - nextly@0.0.2-alpha.21
- - @nextlyhq/ui@0.0.2-alpha.21
- ### @nextlyhq/storage-s3
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list. - ### @nextlyhq/ui
Patch Changes
- #84 `0e17fc6` Thanks @aqib-rx! - Unified schema-migration pipeline with
ui-schema.jsondual-write. - - Migration CLI:
migrate:create/migrate/migrate:check/migrate:status, plusmigrate:downfor forward-resolved rollbacks (DOWN SQL generated at create time, renames preserved). A pooler-safe TTL migration lock replaces the session advisory lock that leaked through Neon's PgBouncer, and production deployments can run pending migrations on boot (db.runMigrationsOnBoot+db.migrateLockTtlSeconds). - -
ui-schema.jsondual-write: the admin Schema Builder always applies changes to the dev database AND writes a committableui-schema.json(the file-only mode is retired). The manifest is now a lossless record of every field option the builder/code-first can set — full validation (min/max length, pattern, etc.), per-field admin (width, description, placeholder…),unique,index, labels, the Draft/Publishedstatusflag (persisted from both the field-change and settings-only save paths), and polymorphicrelationToarrays (previously truncated to the first target). Thetogglefield type round-trips correctly. - - Correct column types:
migrate:createno longer flattens fields before diffing, so hasMany and polymorphic relationships emitjsoncolumns instead of a singletextid column. - - Diffable index/unique migrations (Postgres/MySQL/SQLite): field
unique/index, single-relationship auto-indexes, and the system slug/created_at indexes are now diffed and emitted (CREATE/DROP INDEX) with live-DB introspection, down-migration support, and a backward-compat sentinel so pre-existing tables don't churn. - - Cleanup: removed the unused
verification_tokenstable (a leftover from the retired Auth.js integration; custom auth usesemail_verification_tokensandpassword_reset_tokens).dev:resetauto-detects the dialect fromDATABASE_URL, and the ui-schema field-type set was widened to the full canonical list.
Released all 12 packages at 0.0.2-alpha.20 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
### @nextlyhq/adapter-mysql
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
- Updated dependencies [`f721539`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.20
- ### @nextlyhq/adapter-postgres
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
- Updated dependencies [`f721539`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.20
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
- Updated dependencies [`f721539`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.20
- ### @nextlyhq/admin
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
- Updated dependencies [`f721539`]:
- - @nextlyhq/ui@0.0.2-alpha.20
- ### create-nextly-app
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
### nextly
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
- Updated dependencies [`f721539`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.20
- - @nextlyhq/adapter-mysql@0.0.2-alpha.20
- - @nextlyhq/adapter-postgres@0.0.2-alpha.20
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.20
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
- Updated dependencies [`f721539`]:
- - @nextlyhq/admin@0.0.2-alpha.20
- - nextly@0.0.2-alpha.20
- - @nextlyhq/ui@0.0.2-alpha.20
- ### @nextlyhq/storage-s3
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
### @nextlyhq/storage-uploadthing
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
### @nextlyhq/ui
Patch Changes
- #63 `f721539` Thanks @faisal-rx! - Singles builder popup now auto-derives the slug as kebab-case to match the web convention used by public routes and the entry-form slug validator. Typing
About Pageas the singular name now fills the slug asabout-pageinstead ofabout_page. Collections and components keep their existing snake_case defaults so their backend validators continue to accept the auto-generated value unchanged. The sharedBuilderSettingsModalforwards the per-kind identifier toBasicsTab, where the slug-case helper is selected; a newtoKebabNamehelper lives alongsidetoSnakeNamein@admin/lib/builderfor downstream consumers that need URL-friendly identifiers.
create-nextly-app now resolves the published @nextlyhq/ui and @nextlyhq/plugin-form-builder versions from the npm registry alongside the other @nextlyhq/* packages it scaffolds. Generated package.json files pin both via their published semver range instead of falling back to "latest", so fresh projects install the same versions the CLI was tested against.
Released all 12 packages at 0.0.2-alpha.19 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
### @nextlyhq/adapter-mysql
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
- Updated dependencies [`e2b4131`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.19
- ### @nextlyhq/adapter-postgres
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
- Updated dependencies [`e2b4131`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.19
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
- Updated dependencies [`e2b4131`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.19
- ### @nextlyhq/admin
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
- Updated dependencies [`e2b4131`]:
- - @nextlyhq/ui@0.0.2-alpha.19
- ### create-nextly-app
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
### nextly
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
- Updated dependencies [`e2b4131`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.19
- - @nextlyhq/adapter-mysql@0.0.2-alpha.19
- - @nextlyhq/adapter-postgres@0.0.2-alpha.19
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.19
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
- Updated dependencies [`e2b4131`]:
- - @nextlyhq/admin@0.0.2-alpha.19
- - nextly@0.0.2-alpha.19
- - @nextlyhq/ui@0.0.2-alpha.19
- ### @nextlyhq/storage-s3
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
### @nextlyhq/storage-uploadthing
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
### @nextlyhq/ui
Patch Changes
- #61 `e2b4131` Thanks @zeshan-rx! - Admin UI polish across the Entries forms, Schema Builder, sidebar, and global loaders.
Field width is now respected end-to-end. packFieldsIntoRows no longer treats group as a block-only field, so groups participate in the same row-packing as regular fields and honour admin.width on both the builder canvas and the entry form. FieldRow adds a synthetic spacer column when a row's declared widths sum to less than 100% so partial-width fields keep their authored size instead of stretching to fill, and uses items-start so adjacent fields of different heights align cleanly. NestedFieldGroup in the schema builder uses the shared packIntoRows / parseWidth helpers to render nested children in the same row layout as the top-level canvas; repeater and group containers are forced to full width to stay readable. ComponentRow and GroupInput now delegate to FieldRow + packFieldsIntoRows instead of mapping each child through FieldRenderer directly, so nested component and group fields lay out consistently with the surrounding form. pack-fields-into-rows also guards against undefined / non-array fields input.
Entries table no longer shows the id column by default. getDefaultVisibleColumns keeps id available in the column toggler but excludes it from the initial visible set, matching the rest of the admin's "title first" presentation.
Schema Builder toolbar is now sticky. BuilderToolbar sticks to the top of the builder viewport (sticky top-0 z-30) with a solid background so it stays visible while scrolling long field lists; the collection / single / component builder pages were restructured to render the toolbar outside PageContainer so the sticky positioning has the correct scroll parent, and the container drops its bottom padding to remove the gap underneath.
Sidebar no longer flashes the empty / unauthorised state during hydration. DualSidebar now treats !isHydrated as part of hasPermissionDataPending (alongside the existing permissions-loading / error checks), so menu groups render their loading skeletons until the router and permissions are both ready instead of briefly showing nothing.
PermissionGuard loading state is replaced with a branded loader: a glassmorphic card with an ambient glow, the shared Spinner, and the Nextly brand mark animated via two new global keyframes (brand-orbit, brand-pulse) added to globals.css. A ?debug_loading=true query param force-enables the loading view to make iteration on the loader easier. Auth setup / reset-password / user-management / email-provider secret-field inputs get small consistency tweaks alongside the same loader treatment.
Released all 12 packages at 0.0.2-alpha.18 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
### @nextlyhq/adapter-mysql
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
- Updated dependencies [`de3ec7e`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.18
- ### @nextlyhq/adapter-postgres
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
- Updated dependencies [`de3ec7e`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.18
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
- Updated dependencies [`de3ec7e`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.18
- ### @nextlyhq/admin
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
- Updated dependencies [`de3ec7e`]:
- - @nextlyhq/ui@0.0.2-alpha.18
- ### create-nextly-app
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
### nextly
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
- Updated dependencies [`de3ec7e`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.18
- - @nextlyhq/adapter-mysql@0.0.2-alpha.18
- - @nextlyhq/adapter-postgres@0.0.2-alpha.18
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.18
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
- Updated dependencies [`de3ec7e`]:
- - @nextlyhq/admin@0.0.2-alpha.18
- - nextly@0.0.2-alpha.18
- - @nextlyhq/ui@0.0.2-alpha.18
- ### @nextlyhq/storage-s3
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
### @nextlyhq/storage-uploadthing
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
### @nextlyhq/ui
Patch Changes
- #55 `de3ec7e` Thanks @faisal-rx! - Three related singles / API consistency fixes.
REST responses for collections previously included both snake_case (created_at, updated_at) and camelCase (createdAt, updatedAt) variants of the system timestamp fields. The conversion helper added the camelCase aliases but never removed the snake_case originals, so list and detail endpoints surfaced duplicate keys per row. The snake-to-camel conversion now lives in a single helper, convertTimestampsToCamelCase, exported from shared/lib/case-conversion.ts next to the existing keysToCamelCase / keysToSnakeCase utilities. Both collection-query-service and the singles deserializeJsonFields path call it directly. The previous withTimestampAliases wrapper and its re-export from domains/collections/index.ts are removed. Collections responses now match singles / media / users / api-keys / uploads, which already emitted the camelCase form only.
The admin sidebar's singles list now renders every single in the project rather than capping at the useSingles() default page size of 10. DynamicSingleNav drives a useInfiniteQuery against the singles endpoint and walks subsequent pages while meta.hasNext is true. Each request is bounded to 100 rows so per-request DB load stays small. Secondary consumers that derive visibility or grouping data from the singles list (DualSidebar, DynamicCustomGroupNav, SinglesLandingRedirect) now pass an explicit pageSize: 100 to useSingles, matching the pattern already used by the collections sidebar fetch. This stops the same truncation symptom from hiding section headers or misrouting the /admin/singles landing redirect when the project has more than 10 singles.
The GET /admin/api/singles handler now accepts a 1-based page query parameter as an alternative to offset. The admin UI's shared buildQuery helper emits page for every paginated route; previously the singles endpoint read only offset, so a page change in the Singles builder table left the offset at 0 and the same first page was returned for every navigation. When both offset and page are supplied offset wins, preserving the existing external API contract.
Released all 12 packages at 0.0.2-alpha.17 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
### @nextlyhq/adapter-mysql
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
- Updated dependencies [`4d7b4f7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.17
- ### @nextlyhq/adapter-postgres
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
- Updated dependencies [`4d7b4f7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.17
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
- Updated dependencies [`4d7b4f7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.17
- ### @nextlyhq/admin
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
- Updated dependencies [`4d7b4f7`]:
- - @nextlyhq/ui@0.0.2-alpha.17
- ### create-nextly-app
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
### nextly
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
- Updated dependencies [`4d7b4f7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.17
- - @nextlyhq/adapter-mysql@0.0.2-alpha.17
- - @nextlyhq/adapter-postgres@0.0.2-alpha.17
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.17
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
- Updated dependencies [`4d7b4f7`]:
- - @nextlyhq/admin@0.0.2-alpha.17
- - nextly@0.0.2-alpha.17
- - @nextlyhq/ui@0.0.2-alpha.17
- ### @nextlyhq/storage-s3
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
### @nextlyhq/storage-uploadthing
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
### @nextlyhq/ui
Patch Changes
- #56 `4d7b4f7` Thanks @aqib-rx! - Fix the schema-apply pipeline silently skipping column type changes on Postgres, leaving the live DB permanently drifted while the journal still recorded the apply as successful.
The bug, end-to-end. When a Builder field was reclassified from a text-like type (text, richText, textarea) to a JSON-backed type (group, repeater, blocks, json, chips, point), the diff engine produced a change_column_type operation (text → jsonb on Postgres). That op type was not in the fast in-memory DDL emitter's allow-list, so the pipeline fell back to drizzle-kit's pushSchema. pushSchema considers text → jsonb a non-implicit cast and, in programmatic (non-TTY) mode, omits the ALTER COLUMN … SET DATA TYPE statement from statementsToExecute, returning the omission only in warnings. The pipeline ran the (now-empty or partial) statement list, hit no error, and the migration journal recorded status='success'. The next preview compared the live text column to the desired jsonb token from field-column-descriptor and re-detected the same drift — forever. A site running on Neon (rext-site-v2 / dc_case_studies) ended up with 10 columns stuck on text after three "successful" UI applies on 2026-05-20.
The fix. Four complementary changes in domains/schema/pipeline/:
1. The fast in-memory DDL emitter now owns change_column_type, change_column_nullable, and change_column_default on Postgres. change_column_type emits ALTER TABLE … ALTER COLUMN … SET DATA TYPE <toType> USING "<col>"::<toType> — the explicit USING cast covers the cross-family transitions that Postgres refuses to do implicitly (including the text → jsonb case), and Postgres errors loudly at execution when no registered cast exists between the source and target types. change_column_nullable emits SET NOT NULL / DROP NOT NULL per the toNullable value. change_column_default emits SET DEFAULT <expr> (raw expression, owned by build-from-fields) or DROP DEFAULT when toDefault === undefined. The three op types are added to FAST_PATH_OP_TYPES so they never reach drizzle-kit on Postgres again.
2. The code-first SQL template at sql-templates/postgres.ts (consumed by nextly migrate:create) now emits the same USING "<col>"::<toType> clause for change_column_type. Without this, code-first projects on Postgres would have produced a .sql file in the repo whose ALTER COLUMN … TYPE jsonb failed at nextly migrate apply time in CI — the same drift loop as the Builder UI path, just deferred to migration-apply time. Both consumer surfaces (the apply pipeline and the migration-file generator) now share the same USING contract.
3. Empty op lists on Postgres now also take the fast path (which emits nothing) instead of falling through to drizzle-kit. Letting drizzle-kit handle a "no ops" apply meant it ran its own catalog re-introspection and rename heuristics against the full live DB, and emitted destructive DDL that the diff engine had explicitly decided was not needed. The textarea→richText regression on rext-site-v2 / test_verify_fix surfaced this: both field types map to a text column on Postgres, so the diff produced zero column-level ops, but the slow path then attempted DROP INDEX "single_pricings_pkey" for an unrelated managed table, which Postgres rejects because a primary-key index cannot be dropped directly. Trusting our own diff for "no DDL is needed" closes that surface entirely.
4. A safety net for the slow path (MySQL / SQLite, where the in-memory emitter does not apply, or any future op type that hasn't yet been added to the fast path). After kit.pushSchema(...) returns, the pipeline now inspects pushResult.warnings; when drizzle-kit declined any statement the apply throws a PushSchemaError carrying the warning text, so the journal correctly records a failed apply rather than a false success. Operators see the precise drizzle-kit message instead of an invisible silent skip, and the next apply will not re-detect the same phantom drift.
Affected sites running on a published 0.0.2-alpha.0 … 0.0.2-alpha.16 still need a one-time ALTER TABLE … ALTER COLUMN … SET DATA TYPE jsonb USING … to relabel columns that were created as text during the silent-skip window; the fix prevents NEW drift but does not retroactively repair existing tables (running an Apply through the Builder after upgrading does the relabel automatically). Unit tests cover the three new emitter cases (including identifier-quoting through the USING clause), the routing-eligibility decisions for each (including the empty-ops case), and the safety-net throw path with a representative drizzle-kit warning payload.
Released all 12 packages at 0.0.2-alpha.16 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
### @nextlyhq/adapter-mysql
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
- Updated dependencies [`9bc10b6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.16
- ### @nextlyhq/adapter-postgres
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
- Updated dependencies [`9bc10b6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.16
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
- Updated dependencies [`9bc10b6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.16
- ### @nextlyhq/admin
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
- Updated dependencies [`9bc10b6`]:
- - @nextlyhq/ui@0.0.2-alpha.16
- ### create-nextly-app
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
### nextly
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
- Updated dependencies [`9bc10b6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.16
- - @nextlyhq/adapter-mysql@0.0.2-alpha.16
- - @nextlyhq/adapter-postgres@0.0.2-alpha.16
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.16
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
- Updated dependencies [`9bc10b6`]:
- - @nextlyhq/admin@0.0.2-alpha.16
- - nextly@0.0.2-alpha.16
- - @nextlyhq/ui@0.0.2-alpha.16
- ### @nextlyhq/storage-s3
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
### @nextlyhq/storage-uploadthing
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
### @nextlyhq/ui
Patch Changes
- #52 `9bc10b6` Thanks @aqib-rx! - Fix
update operation failed on table '<table>': value.toISOString is not a functionwhen saving a Single document or a component instance that includes a date field. JSON request bodies deliver date values as ISO strings (e.g."2026-05-20T12:22:29.417Z"), but Drizzle bindstimestampcolumns by calling.toISOString()on the bound value -- so an unmodified string travelling through the adapter blows up at the driver layer.CollectionMutationServicealready coerced date strings intoDateobjects inline at every write site, but the equivalent step was missing fromSingleMutationService.updateand fromComponentMutationService.serializeComponentRow(which feeds every insert / update path in the component service viabuildInsertRowand direct calls).
A new coerceDateFieldsToDate(data, fields) helper in shared/lib/field-transform.ts mutates the row in place, converting string values for field.type === "date" columns into Date objects. Existing Date, null, and undefined values pass through untouched, so the function is idempotent and safe to call on rows that were coerced upstream. The signature accepts a structural ReadonlyArray<{ name?: string; type?: string }> so the same helper covers both FieldConfig[] (singles, components) and the runtime FieldDefinition[] (collections). The helper is wired into single-mutation-service.update before snake-casing the row and into component-mutation-service.serializeComponentRow before column mapping. The six inline copies of the same coercion block in collection-mutation-service.ts were collapsed onto the shared helper as part of the same change so there is one implementation across all three domains. Result: PATCH /admin/api/singles/<slug> with a date field, inserts / updates on components with date fields, and the existing collection flows that already worked all succeed against Postgres, MySQL, and SQLite. Unit tests cover the helper's coercion, idempotency, null / undefined pass-through, and no-touch behaviour for non-date fields.
Released all 12 packages at 0.0.2-alpha.15 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
### @nextlyhq/adapter-mysql
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
- Updated dependencies [`ab23486`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.15
- ### @nextlyhq/adapter-postgres
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
- Updated dependencies [`ab23486`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.15
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
- Updated dependencies [`ab23486`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.15
- ### @nextlyhq/admin
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
- Updated dependencies [`ab23486`]:
- - @nextlyhq/ui@0.0.2-alpha.15
- ### create-nextly-app
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
### nextly
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
- Updated dependencies [`ab23486`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.15
- - @nextlyhq/adapter-mysql@0.0.2-alpha.15
- - @nextlyhq/adapter-postgres@0.0.2-alpha.15
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.15
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
- Updated dependencies [`ab23486`]:
- - @nextlyhq/admin@0.0.2-alpha.15
- - nextly@0.0.2-alpha.15
- - @nextlyhq/ui@0.0.2-alpha.15
- ### @nextlyhq/storage-s3
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
### @nextlyhq/storage-uploadthing
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
### @nextlyhq/ui
Patch Changes
- #51 `ab23486` Thanks @aqib-rx! - Fix users created through the admin "Create user" page being unable to sign in, and clear up the misleading checkbox that caused the silent failure in the first place.
The form's submit handler in packages/admin/src/pages/dashboard/users/create.tsx collected the "Active Account" checkbox value into values.active but never forwarded it to the API, so the backend always saw isActive as undefined and fell back to its default of false. verify-credentials.ts rejects inactive accounts at every login leg, so the newly-created user could authenticate with the right password and still see a generic "invalid credentials" error. The submit handler now sends isActive: values.active ?? true, matching the checkbox's documented "Default: Yes" UX. The backend default of false is intentionally preserved -- it is load-bearing for self-registration via /auth/register, where auth-service.verifyEmail is what flips isActive to true and gates login on proof of email ownership.
The companion checkbox was also reworked. It was labeled "Send Welcome Email" with help text "Send an email with login credentials after account creation", but it actually sets emailVerified: null and dispatches a _verification_ email -- the user could not sign in until they clicked the link. Combined with the form's "Active: Yes" default, that meant the out-of-the-box "create user" flow promised immediate login but silently delivered the opposite. The form field is now named requireEmailVerification, the label is "Require Email Verification", the help text is honest about the verification gate, the default is unchecked (so the form's "Active + immediate login" promise holds end-to-end), the checkbox is disabled when the account is inactive (verification is meaningless for a disabled account), and an inline note surfaces when both flags are on so the admin understands login is still gated until the verification link is clicked. The wire shape is unchanged -- requireEmailVerification maps onto the historical sendWelcomeEmail field at submit time so existing API consumers keep working.
Released all 12 packages at 0.0.2-alpha.14 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
### @nextlyhq/adapter-mysql
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
- Updated dependencies [`ea7fbe5`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.14
- ### @nextlyhq/adapter-postgres
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
- Updated dependencies [`ea7fbe5`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.14
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
- Updated dependencies [`ea7fbe5`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.14
- ### @nextlyhq/admin
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
- Updated dependencies [`ea7fbe5`]:
- - @nextlyhq/ui@0.0.2-alpha.14
- ### create-nextly-app
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
### nextly
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
- Updated dependencies [`ea7fbe5`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.14
- - @nextlyhq/adapter-mysql@0.0.2-alpha.14
- - @nextlyhq/adapter-postgres@0.0.2-alpha.14
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.14
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
- Updated dependencies [`ea7fbe5`]:
- - @nextlyhq/admin@0.0.2-alpha.14
- - nextly@0.0.2-alpha.14
- - @nextlyhq/ui@0.0.2-alpha.14
- ### @nextlyhq/storage-s3
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
### @nextlyhq/storage-uploadthing
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
### @nextlyhq/ui
Patch Changes
- #49 `ea7fbe5` Thanks @aqib-rx! - Fix two related admin-auth failures that surface on hosted databases (Neon, Supabase, PlanetScale, etc.) during transient DB hiccups.
Login/setup fluctuation. The getUserCount dependency in the auth handler bridge used to swallow any DB error and return 0, which made GET /auth/setup-status reply { isSetup: false } whenever a pool cold-start, brief disconnect, or failover landed on this endpoint — the admin route guards then redirected the user to /admin/setup, the next call returned { isSetup: true } once the DB recovered, and the guards redirected back to /admin/login, oscillating until the next hiccup or full page reload. The user count is the bootstrap-gate for two security-relevant decisions (setup-status reporting and the first-admin pre-check), and treating an unknown count as zero also opened a window where a transient DB failure during POST /auth/setup could allow a second super-admin to be created while the real first user was briefly invisible to the query. getUserCount now propagates errors; handleSetupStatus and handleSetup catch them, emit a canonical 503 SERVICE_UNAVAILABLE envelope through the shared buildAuthErrorResponse helper (application/problem+json + x-request-id), and log a structured operator event (setup-status-failed / setup-precheck-failed). The admin's PrivateRoute and PublicRoute now consume a shared lib/auth/setup-status.ts module that fail-safes to "setup complete" on any failure (network error, 5xx, invalid response shape) — staying on the dashboard or login screen is recoverable on the next request, whereas dragging an authenticated user into the setup wizard is destructive. useCurrentUserPermissions is gated by routeType === "private" so its refetchOnWindowFocus cannot fire /me/permissions during a brief Suspense window on a public route.
Intermittent logout around the access-token TTL boundary. The same swallow-and-return-null pattern lived in findUserById, which the refresh handler called after deleting the old refresh token. A momentary DB hiccup at the 15-minute boundary returned null from the lookup, the handler interpreted that as "user is gone" and ran clearAndDeny — clearing both auth cookies and revoking the still-valid session. findUserById now propagates errors; handleRefresh was reordered so all read-only lookups (findUserById, fetchRoleIds, fetchCustomFields) run BEFORE the destructive deleteRefreshToken, and is wrapped in a try/catch that returns 503 SERVICE_UNAVAILABLE on any DB failure with cookies and tokens intact — the client retries on the next request and the session survives. The admin's refreshAccessToken was a boolean primitive that treated every non-200 response (5xx, network errors, our new 503) as "session invalid" and redirected to login; it now returns a tri-state (ok / auth_failed / transient) so authFetch only redirects on a genuine 401 from /auth/refresh and surfaces transient server errors to the caller without logging the user out.
Internal: consolidated four identical build{Login,Register,Forgot,Setup}ErrorResponse helpers into a single buildAuthErrorResponse in handler-utils.ts, fixed a long-standing change-password test mock missing auditLog/trustProxy/trustedProxyIps, and added regression tests covering the 503 path on both setup endpoints, the refresh-handler 503 path (asserting no cookie clearing and no token deletion), and the "no super-admin is created when the pre-check throws" security invariant.
Released all 12 packages at 0.0.2-alpha.13 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
### @nextlyhq/adapter-mysql
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
- Updated dependencies [`f943cb3`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.13
- ### @nextlyhq/adapter-postgres
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
- Updated dependencies [`f943cb3`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.13
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
- Updated dependencies [`f943cb3`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.13
- ### @nextlyhq/admin
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
- Updated dependencies [`f943cb3`]:
- - @nextlyhq/ui@0.0.2-alpha.13
- ### create-nextly-app
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
### nextly
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
- Updated dependencies [`f943cb3`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.13
- - @nextlyhq/adapter-mysql@0.0.2-alpha.13
- - @nextlyhq/adapter-postgres@0.0.2-alpha.13
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.13
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
- Updated dependencies [`f943cb3`]:
- - @nextlyhq/admin@0.0.2-alpha.13
- - nextly@0.0.2-alpha.13
- - @nextlyhq/ui@0.0.2-alpha.13
- ### @nextlyhq/storage-s3
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
### @nextlyhq/storage-uploadthing
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
### @nextlyhq/ui
Patch Changes
- #46 `f943cb3` Thanks @aqib-rx! - Unified upload validation across both upload paths.
/api/medianow applies the same filename hygiene, extension blocklist, MIME allowlist, magic-byte sniff, and SVG sanitization that/admin/api/collections/[slug]/uploadsalready had — previously the global Media endpoint accepted any MIME type and any byte content up to 10MB with no sanitization. Validation logic is extracted intoservices/upload-validation/, bothUploadServiceandMediaServicecall itsvalidateAndSanitizeUploadentrypoint, and every validation failure now throwsNextlyError.validationwith a stable machine code (FILENAME_INVALID,EXTENSION_BLOCKED,MIME_BLOCKED,MIME_NOT_ALLOWED,SIZE_EXCEEDED,MAGIC_BYTE_MISMATCH,SVG_SANITIZATION_FAILED,UNSUPPORTED_FOR_BACKEND). The SVG sanitizer is tightened fromUSE_PROFILES: { svg, svgFilters }alone to explicitFORBID_TAGS(foreignObject,animate*,image,iframe,object,embed,audio,video,source,track,style) plusFORBID_ATTR(event handlers,formaction,xlink:show/actuate) and anuponSanitizeAttributehook that strips anyhref/xlink:hrefwhose value isn't fragment-only (#id). DOCTYPE declarations are stripped before sanitization to defang XML billion-laughs entity expansion, and a 2MB SVG-specific size cap is enforced separately from the general per-file limit. The magic-byte check closes a real polyglot bypass: claimingimage/svg+xmlwith non-SVG bytes (or claiming a non-SVG type with XML bytes) is now rejected before the sanitizer runs.
Breaking: UploadService.upload() now throws NextlyError.validation on validation failures instead of returning { success: false, errors, … } — storage-layer 5xx failures still return the result-shape. /api/media rejects files outside the default MIME allowlist (override via security.uploads.allowedMimeTypes or additionalMimeTypes). SVG uploads with <foreignObject>, external href, animations, <style> blocks, or data: URIs will have those elements stripped — sanitized output may differ from input. @nextlyhq/storage-vercel-blob now supports SVG uploads (previously refused). The adapter returns Vercel Blob's downloadUrl (the file URL with ?download=1 appended) when the upload requests contentDisposition: "attachment", so direct top-level navigation forces an attachment download while <img src> rendering remains unaffected. HTML uploads continue to be rejected with NextlyError.validation (code UNSUPPORTED_FOR_BACKEND, HTTP 415) — they're unsafe to host on a shared blob CDN regardless of disposition. storage-local cannot set per-file headers via Next.js static serving; sanitization still runs so stored bytes are safe, but self-hosters who want strict response headers should serve through a CDN with a response-header policy.
A new structured event nextly.upload.rejected is emitted on every validation failure with { code, route, mimeType, filename, size } so operators can alert on attack-pattern spikes (sudden bursts of MAGIC_BYTE_MISMATCH or EXTENSION_BLOCKED indicate polyglot probing).
Build/dependency: the pnpm.overrides block now bumps undici to ^7 to fix a pre-existing latent runtime bug — jsdom@28 (a transitive dep of isomorphic-dompurify) requires undici@7+'s lib/handler/wrap-handler.js, but the workspace was resolving undici@6.25.0. Any SVG upload through the existing pipeline would have crashed in production; no test exercised that path so it was undetected.
Released all 12 packages at 0.0.2-alpha.12 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers. - ### @nextlyhq/adapter-mysql
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers.
- Updated dependencies [`bbecc0d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.12
- ### @nextlyhq/adapter-postgres
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers.
- Updated dependencies [`bbecc0d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.12
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers.
- Updated dependencies [`bbecc0d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.12
- ### @nextlyhq/admin
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers.
- Updated dependencies [`bbecc0d`]:
- - @nextlyhq/ui@0.0.2-alpha.12
- ### create-nextly-app
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers. - ### nextly
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers.
- Updated dependencies [`bbecc0d`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.12
- - @nextlyhq/adapter-mysql@0.0.2-alpha.12
- - @nextlyhq/adapter-postgres@0.0.2-alpha.12
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.12
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers.
- Updated dependencies [`bbecc0d`]:
- - @nextlyhq/admin@0.0.2-alpha.12
- - nextly@0.0.2-alpha.12
- - @nextlyhq/ui@0.0.2-alpha.12
- ### @nextlyhq/storage-s3
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers. - ### @nextlyhq/ui
Patch Changes
- #43 `bbecc0d` Thanks @faisal-rx! - Fresh projects scaffolded with
pnpm create nextly-appno longer crash at boot under pnpm 10+. pnpm 10 blocks dependency install scripts by default, and without an allowlistbetter-sqlite3never built its native binding, so SQLite scaffolds threwCould not locate the bindings fileon the first admin request.sharp,esbuild, andunrs-resolverwere silently blocked too, producing a slow JS image fallback, drizzle-kit slowness, and an eslint resolver warning respectively. The scaffolder now emitspnpm.onlyBuiltDependenciesin the generatedpackage.json:sharp,esbuild, andunrs-resolveralways, plusbetter-sqlite3when the SQLite adapter is selected. npm, yarn, and bun ignore thepnpm-namespaced field, so it is harmless under those package managers.
Released all 12 packages at 0.0.2-alpha.11 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect. - ### @nextlyhq/adapter-mysql
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect.
- Updated dependencies [`50151bc`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.11
- ### @nextlyhq/adapter-postgres
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect.
- Updated dependencies [`50151bc`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.11
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect.
- Updated dependencies [`50151bc`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.11
- ### @nextlyhq/admin
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect.
- Updated dependencies [`50151bc`]:
- - @nextlyhq/ui@0.0.2-alpha.11
- ### create-nextly-app
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect. - ### nextly
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect.
- Updated dependencies [`50151bc`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.11
- - @nextlyhq/adapter-mysql@0.0.2-alpha.11
- - @nextlyhq/adapter-postgres@0.0.2-alpha.11
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.11
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect.
- Updated dependencies [`50151bc`]:
- - @nextlyhq/admin@0.0.2-alpha.11
- - nextly@0.0.2-alpha.11
- - @nextlyhq/ui@0.0.2-alpha.11
- ### @nextlyhq/storage-s3
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect. - ### @nextlyhq/ui
Patch Changes
- #41 `50151bc` Thanks @aqib-rx! - Fix drizzle-kit rename TUI ("Is
dc_poststable created or renamed from another table?") firing on SQLite and MySQL after the schema-apply scope-reduction landed. The scope-reduction filter iterated by managed-table names and stripped the static system tables thatbuildDrizzleSchemainjects so drizzle-kit's diff recognises them. On SQLite/MySQL drizzle-kit ignorestablesFilter, so the missing system tables looked like drops, paired with the managed adds, and produced the rename TUI on every fresh-install boot — crashing Next.js's non-TTY server thread. The scope-reduction filter now preserves non-managed entries via!isManagedTable(name), restoring the injection's intended effect on every dialect.
Released all 12 packages at 0.0.2-alpha.9 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
### @nextlyhq/adapter-mysql
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
- Updated dependencies [`10479d0`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.9
- ### @nextlyhq/adapter-postgres
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
- Updated dependencies [`10479d0`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.9
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
- Updated dependencies [`10479d0`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.9
- ### @nextlyhq/admin
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
- Updated dependencies [`10479d0`]:
- - @nextlyhq/ui@0.0.2-alpha.9
- ### create-nextly-app
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
### nextly
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
- Updated dependencies [`10479d0`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.9
- - @nextlyhq/adapter-mysql@0.0.2-alpha.9
- - @nextlyhq/adapter-postgres@0.0.2-alpha.9
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.9
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
- Updated dependencies [`10479d0`]:
- - @nextlyhq/admin@0.0.2-alpha.9
- - nextly@0.0.2-alpha.9
- - @nextlyhq/ui@0.0.2-alpha.9
- ### @nextlyhq/storage-s3
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
### @nextlyhq/storage-uploadthing
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
### @nextlyhq/ui
Patch Changes
- #36 `10479d0` Thanks @faisal-rx! - Media URLs returned from the API are now absolute. Previously, the local storage adapter wrote
/uploads/...paths and surfaced them verbatim in API responses — mobile clients, edge workers, and any consumer without the deployment's origin baked in could not resolve the URL. Now,MediaServiceresponses, populatedmediarelations on entry responses, and the collection upload handlers (POST/GET /admin/api/collections/<slug>/uploads) prefix relative URLs withNEXT_PUBLIC_APP_URL(priority:emailConfig.baseUrloverride >NEXT_PUBLIC_APP_URL>http://localhost:3000in dev). Cloud-adapter URLs (S3, Vercel Blob, R2) are already absolute and pass through unchanged. Consumers that previously concatenated the base URL themselves should drop the prefix — double-prefix detection is in place, but the new behaviour means the prefix is no longer needed. The env schema already requiresNEXT_PUBLIC_APP_URLin production, so the localhost fallback is only reachable in development.
Internal: extracted a shared getBaseUrl(override?) helper at src/shared/lib/get-base-url.ts so the email service and the new media-absolutization path resolve through one priority chain. EmailService.getBaseUrl and the new getMediaBaseUrl both delegate to it.
Released all 12 packages at 0.0.2-alpha.8 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution. - ### @nextlyhq/adapter-mysql
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution.
- Updated dependencies [`a5d2af6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.8
- ### @nextlyhq/adapter-postgres
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution.
- Updated dependencies [`a5d2af6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.8
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution.
- Updated dependencies [`a5d2af6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.8
- ### @nextlyhq/admin
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution.
- Updated dependencies [`a5d2af6`]:
- - @nextlyhq/ui@0.0.2-alpha.8
- ### create-nextly-app
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution. - ### nextly
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution.
- Updated dependencies [`a5d2af6`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.8
- - @nextlyhq/adapter-mysql@0.0.2-alpha.8
- - @nextlyhq/adapter-postgres@0.0.2-alpha.8
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.8
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution.
- Updated dependencies [`a5d2af6`]:
- - @nextlyhq/admin@0.0.2-alpha.8
- - nextly@0.0.2-alpha.8
- - @nextlyhq/ui@0.0.2-alpha.8
- ### @nextlyhq/storage-s3
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution. - ### @nextlyhq/ui
Patch Changes
- #34 `a5d2af6` Thanks @aqib-rx! - Fix severe Builder slowness and connection-pool exhaustion when running Nextly against Neon Postgres, and complete the code-first column-delete workflow. Adapter now wires the provider's declared
statementTimeoutMsintopg.Pool(Neon's 30s default was previously ignored, letting stuck queries pin pool slots forever) and bumps Node 20+'s 250 ms Happy Eyeballs per-address timeout floor to 5 s on first connect so transcontinental Neon endpoints stop surfacingETIMEDOUTafter exhausting every resolved address.DB_POOL_MAX/MIN/IDLE_TIMEOUT/QUERY_TIMEOUTenv vars were always documented but never plumbed into the factory — they now flow through with per-field??fallback so each value can fall back to the adapter's dialect-specific defaults (notably the PG adapter'smin: 0for Neon auto-suspend recovery). Boot/HMR drift-check now uses bounded concurrency (3 workers) instead of unboundedPromise.allthat saturated a Neon pool of 5 with 10+ collections. HMRserverComponentChangesevents get a 300 ms trailing debounce so editor burst-saves stop firing a full pipeline per save. A short-lived live-snapshot cache deduplicates the twointrospectLiveSnapshotcalls that previously fired during a single Builder apply, and a missinginstrumentation.tswarning surfaces in dev to nudge users toward the single-worker warmup pattern. A new fast in-memory DDL emitter on PostgreSQL bypasses drizzle-kit's ~10 s catalog re-introspection for the common Builder op set (add_column,add_table), and even on the slow-path fallback the pushSchema call is now scoped to only the table(s) actually touched by the resolved ops rather than every managed table.filterUnsafeStatementsalso blocks orphanDROP SEQUENCE/DROP INDEXwhose inferred owner table is not in the desired schema. A new diff-time default normaliser collapses Postgres's redundant::<type>cast suffix (e.g.'draft'::character varying) and lowercasesnow()so the diff stops emitting phantomchange_column_defaultops for every system column on every apply; a long-standing descriptor drift betweenruntime-schema-generatorandfield-column-descriptor(statustextvsvarchar, missingnow()defaults oncreated_at/updated_at) is also fixed so the new fast path actually triggers in the real Builder flow. End-to-end on a real Neon instance: Builder Save HTTP timing drops from ~11 s to ~5 s and the in-pipeline schema apply drops from ~10 s to ~1.4 s. Code-first column deletes now flow through a newdestructive_dropClassifierEventthat theClackTerminalPromptDispatcherrenders as aDrop "<column>" from "<table>"?confirm in the dev terminal — removing a field fromnextly.config.tsand saving prompts you to confirm before destroying data, matching Drizzle Kit'spushUX;NEXTLY_ALLOW_CODE_FIRST_DROPS=1auto-confirms every drop without prompting for CI/non-interactive workflows. Finally, the API Playground response viewer no longer crashes with "Unrecognized extension value" — the admin bundle was loading two copies of@codemirror/state(6.5.3 + 6.6.0) which brokeinstanceof Extension; apnpm.overridespin forces a single resolution.
Released all 12 packages at 0.0.2-alpha.10 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
### @nextlyhq/adapter-mysql
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
- Updated dependencies [`04da3a7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.10
- ### @nextlyhq/adapter-postgres
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
- Updated dependencies [`04da3a7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.10
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
- Updated dependencies [`04da3a7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.10
- ### @nextlyhq/admin
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
- Updated dependencies [`04da3a7`]:
- - @nextlyhq/ui@0.0.2-alpha.10
- ### create-nextly-app
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
### nextly
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
- Updated dependencies [`04da3a7`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.10
- - @nextlyhq/adapter-mysql@0.0.2-alpha.10
- - @nextlyhq/adapter-postgres@0.0.2-alpha.10
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.10
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
- Updated dependencies [`04da3a7`]:
- - @nextlyhq/admin@0.0.2-alpha.10
- - nextly@0.0.2-alpha.10
- - @nextlyhq/ui@0.0.2-alpha.10
- ### @nextlyhq/storage-s3
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
### @nextlyhq/storage-uploadthing
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
### @nextlyhq/storage-vercel-blob
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
### @nextlyhq/ui
Patch Changes
- #38 `04da3a7` Thanks @faisal-rx! - Fix: variant URLs in populated
media.sizes[*].urlare now absolutized too. The initial absolutization pass only rewrote the top-levelurlandthumbnailUrlfields, so on SQLite — which storesmedia.sizesas TEXT and returns the column as an unparsed JSON string — clients consuminggetMediaVariant(media, "card")on populated entries still received relative/uploads/...paths.absolutizeMediaUrlsnow normalises string-encoded sizes into an object before rewriting variant URLs, so populated media on entry responses returns reachable variant URLs across every dialect. Unparseable JSON resolves tonullrather than leaking the raw string to the API consumer.
Also: toAbsoluteMediaUrl and absolutizeMediaUrls resolve baseUrl lazily — the env-backed default fires only when a relative URL actually needs prefixing. Pass-through cases (absolute URLs, null/undefined/empty) no longer touch the env proxy, so the "absolute URLs unchanged" contract holds in contexts that have not booted env validation (isolated tests, bundler-time analysis).
Released all 12 packages at 0.0.2-alpha.7 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
### @nextlyhq/adapter-mysql
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
- Updated dependencies [`e41725d`, `bd92f1b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.7
- ### @nextlyhq/adapter-postgres
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
- Updated dependencies [`e41725d`, `bd92f1b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.7
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
- Updated dependencies [`e41725d`, `bd92f1b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.7
- ### @nextlyhq/admin
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
- Updated dependencies [`e41725d`, `bd92f1b`]:
- - @nextlyhq/ui@0.0.2-alpha.7
- ### create-nextly-app
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
### nextly
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
- Updated dependencies [`e41725d`, `bd92f1b`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.7
- - @nextlyhq/adapter-mysql@0.0.2-alpha.7
- - @nextlyhq/adapter-postgres@0.0.2-alpha.7
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.7
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
- Updated dependencies [`e41725d`, `bd92f1b`]:
- - @nextlyhq/admin@0.0.2-alpha.7
- - nextly@0.0.2-alpha.7
- - @nextlyhq/ui@0.0.2-alpha.7
- ### @nextlyhq/storage-s3
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
### @nextlyhq/storage-uploadthing
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
### @nextlyhq/storage-vercel-blob
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
### @nextlyhq/ui
Patch Changes
- #32 `e41725d` Thanks @mobeenabdullah! - Internal refactor: consolidate the
packages/nextly/src/services/auth/shim layer. The shim was a directory of one-lineexport *re-exports left over from an earlier reorganisation; the canonical code already lived inpackages/nextly/src/domains/auth/services/. The shim directory has been removed and 29 internal call sites have been pointed at the canonical location. A duplicate test suite of 13 files (mechanical-path-only drift, no logic divergence) has been deleted in favour of the existing copies underdomains/auth/__tests__/. A new@nextly/domains/*TypeScript path alias is added to match the existing@nextly/services/*/@nextly/auth/*pattern. No public exports, runtime behaviour, or wire-format changes; this is shipped as a patch because every package version moves together in the alpha train.
- #30 `bd92f1b` Thanks @mobeenabdullah! -
create-nextly-appnow prompts for a folder name when none is given on the command line. Previously, runningnpx create-nextly-appwith no positional argument was silently treated as "install in the current directory" and then aborted with aDirectory not emptyerror once the user finished the template and database prompts. The CLI now asksWhat should your project be called?withmy-nextly-apppre-filled. You can accept the default with Enter, type any folder name, or type.(or./) to install in the current directory, matching the way the positional argument already worked. When the chosen target directory is non-empty the CLI now offers a three-option recovery prompt (cancel, remove existing files and continue, or ignore files and continue) instead of aborting outright. Theremoveoption preserves any.gitdirectory so existing history is kept.
Note for scripted or CI use: the no-argument form is no longer equivalent to npx create-nextly-app .; it now opens an interactive prompt. If you were relying on the previous behavior in a non-interactive environment, pass . (or any folder name) explicitly.
Released all 12 packages at 0.0.2-alpha.6 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it. - ### @nextlyhq/adapter-mysql
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it.
- Updated dependencies [`338b668`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.6
- ### @nextlyhq/adapter-postgres
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it.
- Updated dependencies [`338b668`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.6
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it.
- Updated dependencies [`338b668`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.6
- ### @nextlyhq/admin
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it.
- Updated dependencies [`338b668`]:
- - @nextlyhq/ui@0.0.2-alpha.6
- ### create-nextly-app
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it. - ### nextly
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it.
- Updated dependencies [`338b668`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.6
- - @nextlyhq/adapter-mysql@0.0.2-alpha.6
- - @nextlyhq/adapter-postgres@0.0.2-alpha.6
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.6
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it.
- Updated dependencies [`338b668`]:
- - @nextlyhq/admin@0.0.2-alpha.6
- - nextly@0.0.2-alpha.6
- - @nextlyhq/ui@0.0.2-alpha.6
- ### @nextlyhq/storage-s3
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it. - ### @nextlyhq/ui
Patch Changes
- #28 `338b668` Thanks @faisal-rx! - Fix
Cannot find package '@nextlyhq/plugin-form-builder'onpnpm devfor blank scaffolds. The base admin page (templates/base/src/app/admin/[[...params]]/page.tsx) and the existing-project admin generator both hard-coded three side-effect imports for@nextlyhq/plugin-form-builder, but the package was only added topackage.jsonon the fresh-scaffold npm path. Blank scaffolds and existing-project installs got the imports without the dep, sonext devfailed at module resolution. The plugin is now opt-in per template: blank ships a plugin-less admin page; the blog template overlays a blog-specific admin page that re-adds the imports (mirroring howformBuilderPluginis registered only in the blog config).generatePackageJsonand the yalc paths ininstallDependenciesaccept aprojectTypeand only include@nextlyhq/plugin-form-builderwhen the selected template uses it.
Released all 12 packages at 0.0.2-alpha.5 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres. - ### @nextlyhq/adapter-mysql
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres.
- Updated dependencies [`fc88dc2`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.5
- ### @nextlyhq/adapter-postgres
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres.
- Updated dependencies [`fc88dc2`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.5
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres.
- Updated dependencies [`fc88dc2`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.5
- ### @nextlyhq/admin
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres.
- Updated dependencies [`fc88dc2`]:
- - @nextlyhq/ui@0.0.2-alpha.5
- ### create-nextly-app
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres. - ### nextly
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres.
- Updated dependencies [`fc88dc2`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.5
- - @nextlyhq/adapter-mysql@0.0.2-alpha.5
- - @nextlyhq/adapter-postgres@0.0.2-alpha.5
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.5
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres.
- Updated dependencies [`fc88dc2`]:
- - @nextlyhq/admin@0.0.2-alpha.5
- - nextly@0.0.2-alpha.5
- - @nextlyhq/ui@0.0.2-alpha.5
- ### @nextlyhq/storage-s3
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres. - ### @nextlyhq/ui
Patch Changes
- #26 `fc88dc2` Thanks @mobeenabdullah! - Collection mutation paths now resolve the physical table through
collection.tableName, honoringdbNameoverrides instead of always deriving the name from the slug. The code-first boot sync detects when a collection's resolvedtableNamediffers from the row indynamic_collections, renames the physical table (Postgres/SQLite/MySQL quotedALTER TABLE ... RENAME TO), writes the new name back, and invalidates the cached Drizzle schema inCollectionFileManagerso the next request rebuilds against the renamed table — previously adbNamechange left CRUD pointing at the stale table until a server restart. When both the old and new physical tables exist, the rename is skipped with a warn so the user can resolve the conflict manually. Component runtime-schema refresh after a UI-driven create/update/apply now flows through the DISchemaRegistry(with a typed fallback to the adapter'stableResolverfor non-DI paths) and surfaces failures as warnings instead of swallowing them in a silent try/catch — the prior behavior leftcomp_*queries selecting pre-rename column names until restart. Generated timestamp columns (createdAt,updatedAt) now emitwithTimezone: false/ plainTIMESTAMPfor Postgres, aligning behavior across SQLite, MySQL, and Postgres.
Released all 12 packages at 0.0.2-alpha.4 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either. - ### @nextlyhq/adapter-mysql
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either.
- Updated dependencies [`af98b55`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.4
- ### @nextlyhq/adapter-postgres
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either.
- Updated dependencies [`af98b55`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.4
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either.
- Updated dependencies [`af98b55`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.4
- ### @nextlyhq/admin
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either.
- Updated dependencies [`af98b55`]:
- - @nextlyhq/ui@0.0.2-alpha.4
- ### create-nextly-app
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either. - ### nextly
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either.
- Updated dependencies [`af98b55`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.4
- - @nextlyhq/adapter-mysql@0.0.2-alpha.4
- - @nextlyhq/adapter-postgres@0.0.2-alpha.4
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.4
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either.
- Updated dependencies [`af98b55`]:
- - @nextlyhq/admin@0.0.2-alpha.4
- - nextly@0.0.2-alpha.4
- - @nextlyhq/ui@0.0.2-alpha.4
- ### @nextlyhq/storage-s3
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either. - ### @nextlyhq/ui
Patch Changes
- #23 `af98b55` Thanks @mobeenabdullah! - Fix Single document fields appearing empty after a component-field rename. Schema-apply and external-schema-update handlers invalidated
["collections"],["entries"],["singles"], and["components"]— but Single document data lives under a separate["single-documents"]namespace (used byuseSingleDocument), which was never invalidated. After a rename,useSingleSchemarefetched with the new field name whileuseSingleDocumentkept serving cached data keyed by the old name, so the form rendereddata[newName]asundefinedand the field appeared blank until a hard refresh. Collections were unaffected becauseuseEntrylives under["entries"], which was already in the invalidation list. The["single-documents"]key is now invalidated alongside the others. Also propagate the Draft/Publishedstatusflag throughbuildFullDesiredSchemafor both collections and singles, mirroring the earlier preview-pipeline fix so the full-schema build path doesn't drop the column either.
Released all 12 packages at 0.0.2-alpha.3 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission. - ### @nextlyhq/adapter-mysql
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission.
- Updated dependencies [`7f4d5d4`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.3
- ### @nextlyhq/adapter-postgres
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission.
- Updated dependencies [`7f4d5d4`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.3
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission.
- Updated dependencies [`7f4d5d4`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.3
- ### @nextlyhq/admin
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission.
- Updated dependencies [`7f4d5d4`]:
- - @nextlyhq/ui@0.0.2-alpha.3
- ### create-nextly-app
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission. - ### nextly
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission.
- Updated dependencies [`7f4d5d4`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.3
- - @nextlyhq/adapter-mysql@0.0.2-alpha.3
- - @nextlyhq/adapter-postgres@0.0.2-alpha.3
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.3
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission.
- Updated dependencies [`7f4d5d4`]:
- - @nextlyhq/admin@0.0.2-alpha.3
- - nextly@0.0.2-alpha.3
- - @nextlyhq/ui@0.0.2-alpha.3
- ### @nextlyhq/storage-s3
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission. - ### @nextlyhq/ui
Patch Changes
- #19 `7f4d5d4` Thanks @aqib-rx! - HTTP read endpoints now return entries/documents regardless of status by default. Previously,
GET /api/collections/<slug>/entries,GET /api/collections/<slug>/entries/<id>,GET /api/collections/<slug>/entries/count, andGET /api/singles/<slug>defaulted to "published-only" and required?status=allto see drafts — confusing for the admin API Playground, which returned 404 for any status-enabled single or collection whose only document was still in draft. The new default is to return all records; pass?status=published(or?status=draft) to filter explicitly. The routes still require authentication, so this only affects callers that already have read permission.
Released all 12 packages at 0.0.2-alpha.2 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits. - ### @nextlyhq/adapter-mysql
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits.
- Updated dependencies [`8e77998`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.2
- ### @nextlyhq/adapter-postgres
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits.
- Updated dependencies [`8e77998`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.2
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits.
- Updated dependencies [`8e77998`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.2
- ### @nextlyhq/admin
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits.
- Updated dependencies [`8e77998`]:
- - @nextlyhq/ui@0.0.2-alpha.2
- ### create-nextly-app
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits. - ### nextly
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits.
- Updated dependencies [`8e77998`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.2
- - @nextlyhq/adapter-mysql@0.0.2-alpha.2
- - @nextlyhq/adapter-postgres@0.0.2-alpha.2
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.2
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits.
- Updated dependencies [`8e77998`]:
- - @nextlyhq/admin@0.0.2-alpha.2
- - nextly@0.0.2-alpha.2
- - @nextlyhq/ui@0.0.2-alpha.2
- ### @nextlyhq/storage-s3
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits. - ### @nextlyhq/storage-uploadthing
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits. - ### @nextlyhq/storage-vercel-blob
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits. - ### @nextlyhq/ui
Patch Changes
- #17 `8e77998` Thanks @aqib-rx! - Fix UI Schema Builder silently dropping the Draft/Published
statuscolumn when editing a collection or single. Saving a field change on astatus: trueentity used to surface a "Rename status → \<new field\>" option (selected by default) becausepreviewDesiredSchemadid not propagate the Draft/Published flag into the desired snapshot — confirming the dialog DROPped the column and every subsequent entry POST withstatus: "published"failed withtable dc_<slug> has no column named status. The flag now flows through the preview/apply pipeline for both collections and singles, so the column survives edits.
Released all 12 packages at 0.0.2-alpha.1 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- ### @nextlyhq/adapter-mysql
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- Updated dependencies [`098d5b1`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.1
- ### @nextlyhq/adapter-postgres
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- Updated dependencies [`098d5b1`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.1
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- Updated dependencies [`098d5b1`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.1
- ### @nextlyhq/admin
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- Updated dependencies [`098d5b1`]:
- - @nextlyhq/ui@0.0.2-alpha.1
- ### create-nextly-app
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- ### nextly
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- Updated dependencies [`098d5b1`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.1
- - @nextlyhq/adapter-mysql@0.0.2-alpha.1
- - @nextlyhq/adapter-postgres@0.0.2-alpha.1
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.1
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- Updated dependencies [`098d5b1`]:
- - @nextlyhq/admin@0.0.2-alpha.1
- - nextly@0.0.2-alpha.1
- - @nextlyhq/ui@0.0.2-alpha.1
- ### @nextlyhq/storage-s3
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- ### @nextlyhq/storage-uploadthing
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- ### @nextlyhq/storage-vercel-blob
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
- ### @nextlyhq/ui
Patch Changes
- #13 `098d5b1` Thanks @mobeenabdullah! - Iterative alpha bump: clean stale @nextly/ in adapter descriptions; contributor bootstrap fix; first OIDC-published release.
Released all 12 packages at 0.0.2-alpha.0 in lockstep (nextly, create-nextly-app, and 10 @nextlyhq/* packages).
What's changed
@nextlyhq/adapter-drizzle
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app ```
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app
- Updated dependencies [`de96251`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.0
- ### @nextlyhq/adapter-postgres
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app
- Updated dependencies [`de96251`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.0
- ### @nextlyhq/adapter-sqlite
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app
- Updated dependencies [`de96251`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.0
- ### @nextlyhq/admin
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app
- Updated dependencies [`de96251`]:
- - @nextlyhq/ui@0.0.2-alpha.0
- ### create-nextly-app
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app ```
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app
- Updated dependencies [`de96251`]:
- - @nextlyhq/adapter-drizzle@0.0.2-alpha.0
- - @nextlyhq/adapter-postgres@0.0.2-alpha.0
- - @nextlyhq/adapter-mysql@0.0.2-alpha.0
- - @nextlyhq/adapter-sqlite@0.0.2-alpha.0
- ### @nextlyhq/plugin-form-builder
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app
- Updated dependencies [`de96251`]:
- - nextly@0.0.2-alpha.0
- - @nextlyhq/admin@0.0.2-alpha.0
- - @nextlyhq/ui@0.0.2-alpha.0
- ### @nextlyhq/storage-s3
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app ```
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app ```
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app ```
Patch Changes
- #4 `de96251` Thanks @mobeenabdullah! - Initial alpha release of Nextly — a TypeScript-first, Next.js-native CMS and app framework.
All 12 packages publish at 0.0.2-alpha.0 in lockstep under the alpha dist-tag.
Highlights:
- Core (nextly) — REST + Direct API, RBAC, hooks, and the runtime engine. API key prefix is nx_live_.
- Admin (@nextlyhq/admin) — Full-featured admin dashboard.
- UI (@nextlyhq/ui) — Headless component primitives shared across packages and plugins.
- CLI (create-nextly-app) — Project scaffolder with blog and blank templates, multi-DB picker, telemetry opt-out.
- Database adapters — @nextlyhq/adapter-postgres, @nextlyhq/adapter-mysql, @nextlyhq/adapter-sqlite, plus the shared @nextlyhq/adapter-drizzle base.
- Storage adapters — @nextlyhq/storage-s3 (also R2 / MinIO / B2 / Wasabi), @nextlyhq/storage-vercel-blob, @nextlyhq/storage-uploadthing.
- Plugins (preview) — @nextlyhq/plugin-form-builder for early exploration; public plugin APIs stabilize at the beta release.
Alpha caveats: APIs may change before 1.0. Pin exact versions in production.
Install:
pnpm create nextly-app@alpha my-app # or npx create-nextly-app@alpha my-app
Stay up to date
Follow Nextly's development and get notified about new releases.