Skip to content

v0.0.2-alpha.43

Pre-release
Pre-release

Choose a tag to compare

@mobeenabdullah mobeenabdullah released this 28 Jul 09:30
87b4e1b

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-sans or 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
    --radius rather 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 migrate can 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, its now()/CURRENT_TIMESTAMP defaults, and PostgreSQL serial sequence defaults — so a second run reported changes to the schema the first run had just written and refused to proceed. A nextval() 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 defaultValue is 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 applyDesiredSchema for MySQL callers.

  • #359 732eb44 Thanks @mobeenabdullah! - A single polymorphic relationship (one whose relationTo lists 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-notification prebuilt 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, with generateStaticParams, generateMetadata, and a reserved-path denylist), isReservedPath, and nextlySitemap / nextlyRobots for the canonical app/sitemap.ts and app/robots.ts. cachedFind now 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:prune command

    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/ui
  • create-nextly-app
  • nextly