v0.0.2-alpha.43
Pre-releaseReleased 17 packages at 0.0.2-alpha.43 in lockstep. Every package below ships at this version.
What's changed
Patch Changes
-
#368
648c7f4Thanks @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-mediumand
font-normalhad 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-borderis 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--radiushad been squaring off. -
#361
7d5a62dThanks @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
6481791Thanks @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 unregisteredmigrate:generate/migrate:run),nextly addno longer tells you to run the removednextly dev, and thedb:sync --forcehelp text states the flag is a deprecated no-op.nextly upgradeandnextly migrate:resolvenow 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
ac3afcaThanks @mobeenabdullah! - A field's declared constantdefaultValueis 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
defaultValuewritten 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
55bc36eThanks @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 exportedapplyDesiredSchemafor MySQL callers. -
#359
732eb44Thanks @mobeenabdullah! - A single polymorphic relationship (one whoserelationTolists 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
c7a3843Thanks @mobeenabdullah! - Retire the insecurewebhook-notificationprebuilt hookThe
webhook-notificationprebuilt hook (selectable in the Schema Builder's
Hooks editor) delivered over a barefetchwith no SSRF protection, and its
secretproduced 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-notificationhook
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
051f660Thanks @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
0c2c369Thanks @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
datatherefore sees the finished document rather than a partial row, which is what makes a rule such asdata.secret !== truemean 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
beforeReadhooks 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-onlySingle is not judged on the response object, which anafterReadhook 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
usernow reach the access rules, on every transport. Acustomrule reading a tenant, a plan or an entitlement sawundefined, 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'sUserContextaccepts them explicitly, along withrolesfor rules that decide on more than one. A claim can never displace the authenticated identity:idandrolescome 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 emptyinlist 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=0shapes 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.readcallbacks 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 definingtoJSON(), for instance — are passed through rather than rejected, so isolating the snapshot never fails a valid write.findSingleandfindSinglesforward yourfallbackLocale. 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 sawundefined.An access rule that writes to its
dataargument 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 !== truereadundefinedand 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
b8bf6d4Thanks @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
--radiuscould 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.CardHeaderalso gained the top-corner counterpart of the fixCardFooteralready 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-xlandrounded-2xlas 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: 0actually resolves to for each step. A new test pins the contract so the documents and the components cannot drift apart unnoticed. -
#363
8de5ea3Thanks @mobeenabdullah! - Content-route reads now enforce publish state and access, and localized draft translations no longer leak.resolveContentandcreateContentRoute(fromnextly/runtime) now default to reading onlystatus: "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 tonotFound()). Pass auserto render member content, oroverrideAccess: truefor a fully trusted read.The Direct API
findgains astatus?: "published" | "draft" | "all"option that drives the same lifecycle-aware filter (constraining a localized collection's per-locale companion status), replacing the previousstatusFieldwhere-clause on the content-route helpers. Status-less collections are handled automatically — the scope is a no-op there. -
#355
521e453Thanks @mobeenabdullah! - nextly now ships content routing and sitemap/robots delivery fromnextly/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
0da91f5Thanks @mobeenabdullah! - Add thenextly webhooks:prunecommandnextly webhooks:pruneruns 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 samewebhooks.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