Changelog
Notable changes to Ratchet.
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Fixed
- Agent chat: a failed turn no longer dumps the raw provider error (often a whole HTTP/JSON response body) into the chat bubble. The full error — stack and
causeincluded — is now logged server-side ([automation] chat turn failed …); the chat shows a single clean line, with a plain-language explanation for the common cases (bad API key, rate limit / quota, provider outage, unreachable provider, oversized conversation, unknown model) and a generic "see the server logs" fallback otherwise. ratchet serve/dev: with aroutes/site opted in, the SSR handler was unreachable — a separatepublicDirrouter mounted at/matched every request first and the localAppnever falls through to a second/mount, so every non-static path 404'd.publicDirserving is now folded intocreateWebRouter.- Static file serving (
publicDir,/_ratchet/*) threwEISDIRon any path that resolved to a directory (e.g./) instead of falling through — nowstat-checks for a regular file. tailwindcssis a direct dependency again — the console/web CSS build shells out to the standalone Tailwind CLI, which resolves@import "tailwindcss"from the top-levelnode_modules; Bun's isolated linker doesn't hoist it there as a transitive dep of@tailwindcss/cli.
Added
createRatchetApp(@egig/ratchet/server) — one function that assembles the whole server (/api/auth,/api/automation,/api,/_site-assets, the console, and thesrc/web/site) in the registration-order-sensitive sequence the router relies on, so that sequence lives in exactly one place instead of being copy-pasted intoratchet serve, the builtdist/server.js, and every Cloudflare/Vercel entry file (where it had already drifted — missing/api/automation,/_site-assets, andstorage). Takes already-constructed infrastructure (db, optionalstorage, optional consoleconsoleAssetssource) plus the new generatedbundle— it does no config loading or dynamicimport(), so it runs unchanged inside a Cloudflare Worker'sfetch(request, env)handler. Features are always mounted; only infrastructure is configurable — the console mounts when you pass an asset source (Vercel omits it and serves those files off its CDN), the web app when you pass its runtime paths.ratchet generatenow also emits.ratchet/app.ts, a barrel exportingbundle({ models, domains, web? }) so an entry file has one import, not four.App+createApiRouter/createAuthRouter/… stay exported for hand-composition./_site-assetsnow mounts just ahead of the console (was just after) so aconsolePath: '/'can't shadow public asset URLs. Deploy guide rewritten around it.- The web app (
src/web/,@egig/ratchet/web) — a code-driven, server-rendered React Router data-mode site, bundled withBun.build(no Vite). Writeroutes/**/*.tsxin a folder convention (routes/root.tsxrenders the whole<html>;about.tsx→/about;blog/$slug.tsx→/blog/:slug;blog/_layout.tsxis a layout; a leading-underscore folder is a pathless layout group;$.tsxis a splat;sitemap[.]xml.tsxescapes a literal dot; a module with aloader/actionand nodefaultis a resource route serving a rawResponse).ratchet generatescans them and emits.ratchet/app-routes.{server,client}.ts;ratchet build/devbundle the client. Single fetch:loader/actionalways run on the server with an injectedcontext({ db, session, settings, registry, storage }+requirePermission(), off the sameratchet_sessioncookie); client navigations fetch one turbo-stream<path>.dataresponse. Streaming SSR viarenderToReadableStream, hydrating the whole document. Config:routesDir(defaultroutes),publicDir(defaultpublic, served at/— a matching static file wins over SSR, handled insidecreateWebRouteritself since the localAppdoesn't fall through a second/mount).ratchet build's bundleddist/server.jsmounts/_ratchetand the web SSR/.datarouter (withpublicDirfolded in) too, via the new@egig/ratchet/web/routerexport, matchingratchet serve— no-op when the site isn't opted into. See the "Web app" guide and ADR 0003. - Console UI primitives (
src/console/client/ui/, experimental) — the first slice of a design-system pass on the console.Button/Input/Label/Dialogare now built on unstyled Radix Primitives (@radix-ui/react-dialog,@radix-ui/react-label) restyled onto a token layer (styles.css's@theme inlineblock —bg-background/text-foreground/border-border/bg-accent/…) withclass-variance-authorityfor variants, in the console's own compact, dense visual language (32px controls, tight spacing). Re-exported from@egig/ratchet/console/clientso a consumer's*.form.tsx/*.input.tsxcan match the console's own look — see that barrel's export comment for the experimental-API caveat. The route-drivenDialog(ModelFormDialog) is rebuilt on the newui/dialog.tsx— same external behavior (Escape/backdrop-click callsonClose), plus a real focus trap and scroll lock.LoginPage/SetupPagemigrated to the new primitives as a worked example. Dark mode: a per-browser light/dark toggle (theme.ts, top-right of the console header) persisted tolocalStorage, defaulting to the OS preference on first visit; a small inline script in the console shell (router.ts) applies it before first paint so there's no flash of the wrong theme. Only the new primitives, both console shells (Layout.tsx's sidebar, andWorkspacePage's own separate header — see that file's comment for why it's a second one), and the auth pages are dark-mode-aware so far — the rest of the console (model list/form pages, the chat panel,RowTable, the comboboxes) still uses plain Tailwind grays and won't re-theme yet; migrating those, plus aSelect/DropdownMenuprimitive and acmdk-based rebuild ofReferenceCombobox/TreeCombobox/ManyToManyMultiSelect, is planned as incremental follow-up work.
Changed
- Breaking: agent runtime rebuilt on the LangChain stack. The hand-rolled provider adapters (
@anthropic-ai/sdk/openaiused directly) and the hand-rolled tool-use loop are gone.run-turn.tsnow wraps LangChain v1'screateAgent(the ReAct agent, built on LangGraph); the model is built by a newsrc/automation/model-factory.tsfrom theProviderrow (ChatAnthropicforkind: 'anthropic'— adaptive thinking +output_config.effort+ advancing ephemeral prompt-cache all still forwarded;ChatOpenAIon the chat-completions API forkind: 'openai',configuration.baseURLfromProvider.url, still covering every OpenAI-compatible host). The RBAC tool layer (tool.ts,resolveAgentTools/executeAgentTool) is unchanged — each tool call still runs through the target model's pipeline re-authenticated as the chatting user.Message.content(assistant-ui parts) and theassistant-streamwire protocol are unchanged — no database migration. New required deps:langchain,@langchain/core,@langchain/langgraph,@langchain/anthropic,@langchain/openai;@anthropic-ai/sdkandopenaidropped as direct deps.@egig/ratchet/automationno longer exportsChatProvider/ChatRequest/ChatMessage/ToolSpec/resolveProvider/parseToolInput(internal plumbing, zero external consumers); it now exportscreateChatModel. SettingLANGSMITH_TRACING=true+LANGSMITH_API_KEYgives full per-turn tracing (inert otherwise). The/api/automationroutes now require a Node-compatible runtime — see the Deploy guide. - Breaking: there is no built-in
websitedomain — Ratchet neither renders pages nor ships aPage/Contactmodel orWebsiteDomainsettings. Rendering issrc/web/(below); the content is scaffolded source.ratchet initnow writesmodels/website/— aPagemodel (slug,title,metaDescription, abodytext field sanitized on every write withsanitize-html,status,navLocation/navOrder), aContactmodel (name/email/message/status, no public write endpoint — the scaffoldedroutes/contact.tsxactioninserts rows through its loadercontext.dbwith its own honeypot + validation), and asettings.domain.tsdeclaring thewebsiteDomain Settings (title,description,siteUrl,noindex,favicon,ogImage) — all editable source in the consumer's project. No first-run seeding: create pages in the console. - Removed:
field.custom('richtext', …)and the console's Quill rich-text editor.Page.bodyis a plainfield.text()(a<textarea>in the console);quillandsanitize-htmlare no longer@egig/ratchetdependencies. ratchet initnow scaffolds a working public site:routes/root.tsx(nav from the scaffoldedpagestable +websitesettings), a hand-authoredroutes/index.tsxlanding page,routes/$.tsx(renders a publishedPageby slug, 404s otherwise),routes/contact.tsx(a working contact form — serveraction, honeypot, inserts acontactsrow),public/theme.css(a production-ready light/dark theme), and themodels/website/set above. Addsreact/react-dom/react-router/sanitize-htmlto the scaffoldeddependencies.- Breaking: Hono removed — every router (
createApiRouter,createAuthRouter,createAutomationRouter,createConsoleRouter,createSiteAssetsRouter) now returns a small localApp(ratchet/router's newApp/Ctx,router/http-app.ts) built directly on the Fetch API'sRequest/Response— the same "no framework, justBun.serve+the platform" style as a hand-rolled Data Mode React Router server, rather than a general-purpose routing framework.Appsupports the sameapp.get/post/patch/delete,app.route(prefix, subApp),app.onError,app.fetch, and (for tests)app.request(path, init)this package's own routers and test suite already relied on, soratchet serve/ratchet dev/ratchet build's generated bundle all keep working unchanged from a consumer's point of view — but any code that imported Hono's ownContext/Honotypes, or reached intohono/cookie/hono/utils/mimedirectly, needs to switch toratchet/router'sCtx/setCookie/deleteCookieequivalents.hono/@hono/node-serverare no longer dependencies of@egig/ratchetor of aratchet init-scaffolded project;ratchet build's generateddist/server.jsnow boots via a newserveNode(ratchet/router), a minimal Nodehttp-to-Fetch bridge that preserves the existing "runs on plain Node, no Bun required" VPS/container deploy story. - Docs site migrated from VitePress to Fumadocs on React Router:
docs/is now a React Router (framework mode, SPA/prerendered, matching the console's own router) app with its ownpackage.json, installed as part of the repo's Bun workspace. Content lives underdocs/content/docs/*.mdx; the same guides, at the same/docs/...URL shape (/guide/*->/docs/*).
Added
- Agent read tools: an
Agentwhose role holds areadgrant now gets two builtin tools per granted resource —list_<model>(JSON-shapedfilters/sort/limit/offset/includeparams, returns the same{ data, meta }envelope asGET /api/:model) andfindOne_<model>(one row by id). They run through the exact samelistRows/getOneRowpath the REST GET routes use, so field-levelreadgrants,?include=relation filtering, andapi.ownerFieldscoping all apply identically — a chatting agent can't read anything the driving user couldn't.action: '*'now expands to these plus the existing write tools. - Multi-column sort:
?sort=now takes a comma-separated, priority-ordered list of keys —?sort=status,-createdAtsorts bystatusascending, thencreatedAtdescending.id/createdAt/updatedAt/createdByIdare always sortable (previously onlyindexed: truefields were). - Console: sortable column headers plus a "Sort" panel next to "Filter" — click a header to sort by it (cycles asc → desc → off), shift-click to add it as a secondary key, or compose the full ordered list in the panel. Works on model list pages (as a shareable
?sort=URL overlay) and on workspace tabs (persisted to the saved view). - Granular, per-field permission:
Permissionrows can now name afield(resource/action/field, any of which may be'*') to grant a role read/write access to individual fields of a model, not just whole resource:action pairs. ilikefilter operator — the case-insensitive form oflikefor string/text fields (?filter=[["name","ilike","%ada%"]]).Workspace.chatEnabled(defaultstrue): a persistent per-workspace setting that removes the console's agent chat panel and its show/hide toggle entirely when off, distinct from the per-browser hide toggle.- Console:
referencefields now render as a searchable combobox instead of a plain<select>— typing filters server-side (ilikeon the target model'sdisplayField) when that field is an indexed string, and falls back to client-side filtering of the first 100 rows otherwise. - Console: workspace tabs can be renamed inline (double-click the tab label); the sidebar account menu has a "Workspace" link back to the signed-in user's workspace.
- Custom operations: a model can now declare named operations beyond
create/update/remove(e.g. alock/unlockbutton that's really anupdatewith a fixed field value) as extra keys inoperations, dispatched by a new genericPOST /:model/:id/:operationroute.presetFields()(ratchet/auth) is the sugar helper for the common "write these fixed fields" case; a custom operation can also declareparams(validated request input, samefield.*()DSL as model fields) and aconsoleblock (label, confirm, placement, a data-drivenvisibleWhen) controlling how it renders as a button — with a param-taking operation auto-rendering a small modal form — in the generated console. See the "Custom Operations" guide. - Console: custom model forms: a
<name>.form.tsxundermodelsDir(<name>being a model's own name, e.g.customers.form.tsx) replaces that model's generated create/edit form entirely —ratchet generatecollects them into a registry the console client bundle imports, rejecting an unmatched or duplicate name. It receivesfields, the model's own fields each pre-bound to their built-in editor (fields[name].render({ value, onChange, error? })renders areferencedropdown/fileupload/manyToManymultiselect/etc. without switching on field kind by hand;fields[name].metais that field's metadata), plusgetRow/createRow/updateRow/useModels/useAuth/FieldInputexported from@egig/ratchet/console/clientfor everything else — so a custom form doesn't have to reinvent the generated one's building blocks. A custom form's own Tailwind classes are scanned into the console bundle's stylesheet the same way the framework's own components are. See the "Console" guide's "Custom forms" section. - Console: custom field inputs: a
<model>.<field>.input.tsxundermodelsDir(e.g.customers.email.input.tsx) replaces just that field's input — everywhere it would normally render (the generated form, andfields[name].render(...)in a custom form, above) — with no change to the model definition needed, unlike the existing model-declaredfield.custom(name, base)(which this now takes priority over).ratchet generaterejects an unmatched model/field, a malformed filename, or two inputs for the same model+field. See the "Console" guide's "Custom field inputs" section. field.tree(): a self-referencing parent-pointer hierarchy on a model — for aCategorytree, a Chart-of-AccountsAccount, an org chart'smanagerId, or any other tree-shaped data. A model may declare at most one;defineModel()resolves its target to the model's own name automatically.?include=parentembeds the parent row,?filter=/?sort=work likereference's, and every write is checked for cycles (reparenting a node under itself or one of its own descendants is rejected withTREE_CYCLE) before it commits. The console renders it as a searchable tree picker (parent / childbreadcrumb labels) that excludes the record being edited and its descendants from the option list. See the "Models & Fields" guide's "Tree / hierarchy fields" section.- Root admin onboarding (
POST /api/auth/setup) now also provisions the framework's first built-inAgent, named Ratchet, wired to the newRootrole so it can call every tool from turn one. SinceAgent.providerIdis required, setup additionally collects aproviderApiKey(plus optionalproviderKind/providerUrl) and creates thatProviderin the same transaction — a fresh instance ends setup with a chat-ready assistant instead of an emptyAgentslist. The console's/setupform has matching provider fields. filefield storage is now driven by flystorage, with well-known cloud backends configurable declaratively via a newstoragekey inratchet.config.ts—{ driver: 's3' | 'gcs' | 'azure', ... }(S3'sendpoint/forcePathStylealso cover S3-compatible services: R2, MinIO, DigitalOcean Spaces, Backblaze B2).ratchet servebuilds the configured backend automatically via the newbuildStorageAdapter(ratchet/storage), the same helper any other Node/Bun entry file can call. Omittingstoragekeeps today's zero-config local-fs default. Each cloud driver's adapter + SDK is a peer dependency behind its own subpath (ratchet/storage/s3,/gcs,/azure) so picking one doesn't require installing the others.- Reading a
filefield's bytes back (GET /api/:model/:id/:field) now streams directly from the storage backend instead of buffering the whole file into memory first.
Changed
ratchet generatenow also runsdrizzle-kit generate, emitting SQL migration files from the fresh schema diff.ratchet migrateis correspondingly narrowed to onlydrizzle-kit migrate— it no longer regenerates the schema or diffs it, just applies the pending migration files. The full workflow isratchet generate(review the SQL) →ratchet migrate(apply it).- Breaking: the framework's tooling and runtime moved from Node/npm to Bun —
ratchet(includingratchet serve) now runs under Bun, package installs usebun install, and the framework's own build/test scripts useBun.build/bun testinstead of esbuild/tsx/vitest. Consumer apps need Bun 1.3+ installed;ratchet initscaffolds a Bun-basedpackage.jsonaccordingly.ratchet build's generateddist/server.jsstill targets plain Node, so a VPS/container deploy doesn't need Bun. - Breaking: the generic
/api/:modelrouter now requires a matchingPermissionrow for every route by default, including reads (a new implicit'read'action) — previously only create/update/remove were gated, and only when a model author manually composedrequireAuth/requirePermissioninto its pipeline. A model that must stay reachable without a session opts out via the newapi: { public: true }. - Breaking: field-level access is secure-by-default — a role with a
(resource, action)grant but no matchingfieldgrant gets zero fields, not every field. ExistingPermissionrows need afield: '*'added (or per-field rows) to keep working after upgrading; the bootstrap Root role created byPOST /api/auth/setupalready does this automatically. requireAuth/requirePermissionno longer need to be composed by hand into a model's ownoperations— the router applies both automatically. They're still exported for custom/dedicated routers that bypass the generic router entirely (e.g. an agent tool call,automation/tool.ts).- Breaking:
Workspacefreezes/unfreezes a row vialock/unlockcustom operations (built onpresetFields(), above) instead of a plainPATCH { locked: … }— a role needs its ownlock/unlockgrant in addition to theupdate+lockedfield grant it already needed. The console's "Lock workspace"/"Unlock workspace" button calls the new operations. - Breaking: a bare
?sort=onGET /api/:modelis now offset-mode (responsemetais{ total, limit, offset }, just ordered) instead of switching to cursor-mode. Cursor-mode pagination now requires an explicit?cursor=(pass it empty for the first page) alongside a single-key?sort=. - Breaking:
workspace_views.sortField+sortDirectionare replaced by a singlesortJSONB column holding an ordered[{ field, direction }]list. Consumer apps must re-runratchet migrate; theupdate_workspace_viewsagent tool now takessortinstead of the two scalar fields. - Breaking:
POST /api/auth/setupnow requires aproviderApiKeyfield (see the built-inRatchetagent, above) — any script or test fixture calling it directly needs to add one. - Breaking:
createApiRouter'sstorageparameter, and everyFileStorageAdapterimplementation, is replaced by flystorage's ownFileStorage(see "filefield storage", above) — no compatibility shim.ratchet/storage/node's factory is renamedcreateLocalStorage(wascreateNodeFsStorageAdapter) and now returns aFileStorage; a hand-writtenFileStorageAdapter(e.g. for a binding flystorage has no adapter for, like Cloudflare R2's) needs to become a flystorageStorageAdapterinstead — seeexample/deploy/cloudflare/worker.ts'sR2StorageAdapterfor the new shape.
Fixed
ratchet dev/ratchet buildno longer shell out tonpx tailwindcssfor the console stylesheet — it invoked the Tailwind CLI as if it were a consumer dependency and failed withcould not determine executable to runin any app that didn't also install@tailwindcss/cli. The framework now resolves and runs its own bundled@tailwindcss/cli.ratchet devno longer prints[dev] server exited with code 130on Ctrl-C — the interrupt reaches the spawned server directly through the terminal, and that (plus signal kills) is now recognized as a deliberate shutdown rather than a crash. Shutdown also no longer hangs when the server has already exited.
v0.1.0 - 2026-08-24
Initial release.
Added
- Model definitions (
defineModel(),field()) that generate a Drizzle schema, Zod validators, and a model registry. - Generic REST API —
GET/POST/PATCH/DELETEat/api/:modelwith filtering, sorting, cursor and offset pagination, and?include=relations. - Composable pipelines:
create/update/removeaspipe(...)chains aroundvalidateandpersist. - Session-based auth router (register/login/logout/me) with role/permission checks.
- Generated console SPA, mountable via
createConsoleRouter. ratchetCLI (build,generate,migrate,serve).- Runtime-agnostic routers (
createApiRouter,createAuthRouter,createConsoleRouter) usable outside Node, alongside Node-onlyratchet build/servetooling. - VitePress documentation site, deployed to GitHub Pages.