Release History

Changelog

All notable changes to Grit are documented here. Each release includes new features, bug fixes, and any breaking changes you need to be aware of.

v3.31.75July 6, 2026

Multi-image fields on mobile. A files field (name:files:image) now renders a proper multi-picker: select several photos from the gallery at once, see them as a grid of removable thumbnails, each uploaded in the background, and the payload carries an array of file references. Single file fields stay single-select. The picker sheet already supported multi-select; generated forms now use it for array fields.

v3.31.74July 6, 2026

Searchable select for relationships on mobile. A belongs_to field in a generated form used to render every related record as a horizontal row of pills, which falls apart once there are more than a handful. It now uses a new RelationSelect component: a tidy select that opens a bottom sheet with a pinned search box and a scrollable, filtered list — pick one and it fills in. Wired into every generated resource form; regenerating a resource with a relationship picks it up.

v3.31.73July 6, 2026

MinIO is now reachable from mobile devices. The dev docker-compose.yml published MinIO on 127.0.0.1:9002 (localhost only), so a phone or emulator couldn't load uploaded images even though the API (bound to all interfaces) worked fine — list and detail thumbnails stayed blank. It now binds 9002:9000 on all interfaces, so devices on your LAN can fetch stored images. Pairs with resolveImageUrl() (v3.31.72), which rewrites the localhost host to your dev IP. Existing projects: change the minio ports to "9002:9000" / "9003:9001" and docker compose up -d minio.

v3.31.72July 6, 2026

Image previews everywhere on mobile. Generated resources now show pictures throughout: an instant local preview in the create/edit form the moment you pick a photo (with an upload spinner overlay), a thumbnail column in the list table, and the hero image on the detail screen.

New lib/images.tsresolveImageUrl() fixes the classic dev gotcha: MinIO hands back http://localhost:9002/... URLs that a device or emulator can't reach (localhost = the device itself). It rewrites the host to the same dev host the app already uses for the API, so stored images actually load — while real S3/R2 public URLs pass through untouched. Every generated list, detail and form image runs through it; regenerating a resource adds it.

v3.31.71 · critical fixJuly 6, 2026

Fix: request bodies larger than 4 KB were silently truncated. Pulse's error-tracking middleware captures a request-body snippet for error context, but it restored only that snippet to the request — discarding everything past MaxBodySize (default 4096 bytes). Every request that sends a Content-Length (mobile apps, native/CLI clients, curl) reached handlers with a body cut to 4 KB, so file uploads and any large JSON POST failed with confusing "no file" / parse errors. Browsers were unaffected because fetchsends multipart chunked (no Content-Length), which skipped the capture — which is why the web dropzone always worked while mobile never did.

The scaffold now mounts Pulse with WithRequestBodyCaptureDisabled(), so the full body always reaches your handlers. This affects every generated API; regenerate or add that option to your Pulse mount. Mobile image uploads (avatar, resource forms, imports) now work end-to-end.

v3.31.70July 6, 2026

A proper image picker for mobile forms. Tapping an image field used to jump straight into Android's system crop screen — whose only button was CROP, with no clear "use this photo" and no permission prompt of our own.

Now it opens a clean, themed picker sheet: choose Library or Camera (with a friendly permission prompt and an "Open Settings" fallback if access is off), then preview the selection and decide — Use photo, Crop (the native editor, only when you ask for it), or choose a different one. The dropzone shows a spinner while the upload runs. The generated resource form uses it for every image field; regenerating any resource upgrades an existing app.

v3.31.69July 6, 2026

Fix: image uploads from the Expo app. Uploads were failing with 400 "No file provided" because expo-file-system's uploadAsync sends an empty body under the New Architecture on SDK 54 (the request landed in a few ms with no file). The mobile upload helper now uses fetch + FormData with a React Native file descriptor and — crucially — never sets Content-Type by hand, so fetch keeps the multipart boundary intact. Fixes avatar, blog and every generated resource-form image field.

The /uploads handler is also more robust: it falls back to the first file part under any field name and logs the request's content-type + fields when a file is genuinely missing, so client-side multipart problems are diagnosable from the server terminal. Re-run pnpm i in apps/expo after updating (the helper no longer needs expo-file-system for uploads).

v3.31.68July 6, 2026

Background CSV import — imports now run server-side and survive leaving the screen. The import endpoint no longer blocks: it reads the upload, creates a job, processes rows in a goroutine and returns 202 with a job id.

Backend (every architecture)

A shared ImportJob table tracks every resource's imports. POST /<plural>/import kicks the work off and returns immediately; a new shared GET /imports/:id reports live processed / total plus the final created / skipped / failed counts and per-row errors, so a large file never times the request out.

Mobile

Imports run in a module-level store, so they keep uploading and polling even after you close the import sheet. A persistent progress banner shows every in-flight import across navigation, then the result — tap "Continue in background" and carry on using the app. Regenerating any resource upgrades an existing app to the new flow.

v3.31.67July 3, 2026

CSV import — bulk-create records from a spreadsheet. grit generate resource now generates a bulk import endpoint (all architectures) plus a full mobile import flow.

Backend (every architecture)

Each resource gets POST /<plural>/import (upload a CSV → typed bulk-create) and GET /<plural>/import/template (a ready-to-fill header CSV). Everything is optional except model-required fields: file columns are skipped; a belongs_to is given by name (a category column, not category_id) and the related record is looked up and created if missing; rows that hit a unique constraint are skipped (safe to re-import) and other failures are reported per-row.

Mobile

The resource list gains an import action that opens a sheet: download the template, pick a CSV, preview the parsed rows, import with a progress bar, then a summary of created / skipped / failed with per-row errors. Adds expo-document-picker — run pnpm i. Background (async) import is the final step.

v3.31.66July 3, 2026

Mobile: relationship filters. Resource lists with a belongs_to field gain a funnel action that opens a filter sheet.

The sheet shows a picker per relationship (loaded from the related resource); pick a value to scope the table (?<fk>=<id>, which the API already supports), with an All chip and a Clear all. The funnel shows a dot while filters are active, and export respects them. Resources without a relationship simply don't show the funnel. Re-run grit generate resource to pick it up. CSV import is the last piece.

v3.31.65July 3, 2026

Mobile: CSV export. Every generated resource list gains a download action that exports the data as CSV and opens the native share sheet.

Tapping export downloads /<plural>/export (honouring the current search) to a file and hands it to the OS share sheet — mail it, save it, open it in Sheets. New lib/export.ts helper built on expo-file-system + expo-sharing (run pnpm i). Filters and CSV import land next.

v3.31.64July 3, 2026

Mobile: scrollable data table. Generated resource lists now render as a horizontally-scrollable table — a column per field with tap-to-sort headers — instead of cards.

The title field leads (bold), followed by a column for every scalar and belongs_to field; dates, numbers and booleans format per cell. Tap a sortable header to sort (wired to the API's sort_by / sort_order, which the list hook now accepts), tap a row to open the detail. Search, infinite-scroll pagination and the quick-create sheet are unchanged. Re-run grit generate resource to switch a list to the table.

v3.31.63July 3, 2026

Mobile: quick-create bottom sheet. Adding a record no longer always means a full-screen navigation — the resource list's + now opens a slide-up sheet with the form, while detailed edits stay a full page.

New shared FormSheet component — a themed, keyboard-aware bottom sheet built on React Native's Modal (no extra dependencies). The generated list renders the same <Name>Form inside it for a fast add; the detail screen's Edit still opens the full page for longer records. One form, two containers. Re-run grit generate resource to pick it up.

v3.31.62July 3, 2026

Mobile: full CRUD on generated resources. Generated resources now support edit, update and delete, not just create + read.

grit generate resource now emits a shared <Name>Form component (in components/resource-forms/) that both the create and edit screens render — the create page and a new app/<plural>/edit/[id].tsx screen pre-fill from the record and drive useCreate / useUpdate. The detail screen gains Edit and Delete (with a confirm) actions. The shared form is container-agnostic, ready to drop into a bottom sheet next. Because generation is now idempotent, re-run grit generate resource <Name> --fields … to add CRUD to an existing resource.

v3.31.61July 3, 2026

Mobile: built-in Blog & User resources. The scaffolded Expo app now surfaces the framework's built-in Blog feature and lets you add users — no admin panel required.

The More tab's Resources section now leads with Users and Blogs (alongside your generated resources). Blogs get a paginated, searchable list plus a create screen (title, excerpt, content, cover image upload, publish toggle) backed by /admin/blogs — the posts grit seed already creates now have a home. The Users list gained a + to create a user (name, email, password, role, active) via /admin/users.

v3.31.60July 3, 2026

generate resource is now idempotent. Re-running grit generate resource <Name> for a resource that already exists used to append duplicate injections — duplicate switch cases, routes and exports — which broke the API build. Now every injection is skipped when it's already present.

The two low-level inject helpers gained a whitespace-insensitive "already there?" guard, so a second run finds each of its injections in place and does nothing (files are also just overwritten). Safe to re-run to pick up regenerated files, or after editing a resource's fields.

v3.31.59July 3, 2026

Mobile: create forms + a More tab hub. Generated mobile resources can now add data, not just browse it — so a --mobile project no longer needs an admin panel to get started.

Create screen per resource

grit generate resource now also scaffolds app/<plural>/new.tsx — a form wired to the generated useCreate<X> mutation, with an input per field: text / number / textarea / toggle for scalars, an image picker (upload → FileRef) for file fields, and a chip picker for belongs_to relationships. The list screen gains a + button to reach it.

"More" tab

The mobile Explore tab is now More (with an ellipsis icon) and acts as the app hub: a Resources section that grit generate resource injects each new resource into, plus the Users / Storage / Analytics / Notifications tools. Restart nothing — reload the Expo app.

v3.31.58July 3, 2026

Fix: mobile file uploads ("No file provided"). Avatar / image uploads from the Expo app failed with a 400 even though a file was selected.

Two causes, both fixed. On the client, React Native's fetch + FormData (RN 0.81 / Expo SDK 54) can drop the file part entirely; the upload helper now uses expo-file-system's native uploadAsync, which streams the file reliably. On the server, the audit-log middleware read the entire request body to digest it — wasteful for a binary upload and enough to leave ParseMultipartForm with nothing to parse; it now skips multipart/form-data bodies. Upload errors also surface the server's actual reason now instead of a generic failure. Adds expo-file-system — run pnpm i, restart the API, and npx expo start -c.

v3.31.57July 3, 2026

Fix: mobile mutations blocked by CSRF (403). Native clients could read data but every write — file uploads, generated-resource create/update/delete, profile updates — failed with a 403 CSRF_INVALID.

React Native's fetch (and Android's OkHttp) transparently store and resend the grit_access cookie the API sets at login. TheAutoCSRF guard saw that stray cookie and treated a bearer-authenticated request as cookie-authenticated, demanding a CSRF token the app never sends. The guard now skips CSRF whenever an Authorization: Bearer header is present — an explicitly-authenticated request can't be forged cross-site, so it's CSRF-immune regardless of a tag-along cookie. On an existing project, restart the API after pulling the fix.

v3.31.56July 3, 2026

Mobile code generation. grit generate resource now scaffolds the mobile app too, not just the backend, web hooks, and admin.

Generated Expo screens & hooks

When a project has an apps/expo app, generating a resource also writes a typed React Query hook (infinite-scroll list, single item, and create/update/delete mutations), a paginated list screen, and a detail screen — all field-aware (images become thumbnails, a belongs_to renders its related record, dates/bools/files format sensibly). A shared safe-area ScreenHeader with a back button ships with the scaffold.

Relationship filtering

Every belongs_to resource is now filterable by its foreign key — GET /products?category_id=… returns just that parent's children, and the generated hook takes the same filter. This is what powers a real category → products browse flow on mobile.

Mobile app polish

The scaffolded Expo app gained a full light/dark theme (default light, with a working Settings toggle), the Grit logo on auth, a floating glass tab bar lifted above the Android system nav, safe-area page headers, wired-up Explore destinations (Users with pagination, Notifications, Storage, Analytics, Content, Integrations), and profile avatar upload + change password. Also fixed: the physical-device API URL (derived from Expo's host), a splash-screen hang, the auth token shape, and post-login navigation. Adds expo-image-picker, expo-linear-gradient, expo-blur, and react-native-css-interop — run pnpm i then npx expo start -c.

v3.31.55July 3, 2026

Redesigned mobile auth & navigation. The scaffolded--mobile app now ships a premium, production-grade UI out of the box instead of the plain starter screens.

Polished login & register

Both auth screens are rebuilt as a single elevated card on a faint architectural grid: a gradient brand header, icon-prefixed inputs, a show/hide password toggle, inline validation, a gradient primary CTA, and Google sign-in. Every action fires haptic feedback and the card animates in with a spring FadeInUp.

Floating glass tab bar

The bottom navigation is now a floating, rounded bar with a native frosted-blur background on iOS (solid elevated surface on Android) and a selection haptic on every tab switch. A new PressableScale primitive gives buttons the tactile spring-press micro-interaction. New dependencies:expo-linear-gradient and expo-blur — runpnpm i then npx expo start -c.

v3.31.54July 3, 2026

Mobile styling fix (NativeWind). A scaffolded--mobile app rendered completely unstyled — raw text on a black screen — and hung on startup.

The Expo app was missing its Babel config

NativeWind requires a babel.config.js with thejsxImportSource: "nativewind" option and thenativewind/babel preset — without it every className is silently ignored. The scaffold never generated that file. It now ships one, plus the react-native-worklets dependency and its Babel plugin (required by Reanimated 4, whose absence caused the startup hang), andweb.bundler: "metro" in app.json. On an existing project, add apps/expo/babel.config.js, installreact-native-worklets, then restart Metro with a clear cache:npx expo start -c.

v3.31.53July 3, 2026

Mobile (Expo) scaffold fixes. A fresh --mobileapp now starts cleanly with correct dependency versions and real app icons.

App icons & splash now ship with the scaffold

app.json referenced ./assets/icon.png and./assets/splash.png, but those files were never generated — so Metro failed with “Unable to resolve asset”. The Grit logo is now embedded in the CLI and written to icon.png,splash.png, adaptive-icon.png, andfavicon.png on scaffold (with matching Android adaptive-icon and web favicon entries in app.json). The same brand logo is also dropped into the web, admin, and single-app public/ folders.

Expo dependency versions aligned to SDK 54

The Expo app pinned expo 54 but shipped SDK-53 versions of a few packages, triggering compatibility warnings. Bumpedexpo-image (~3.0.11), expo-haptics (~15.0.8),expo-web-browser (~15.0.11), react-native (0.81.5), and typescript (~5.9.2). On an existing project you can also runnpx expo install --fix.

v3.31.52July 1, 2026

Self-update fix. grit update could report success but leave no grit on your PATH.

go install now targets grit's real location

When grit was installed via the install script (into ~/.grit/bin), the update's go install step wrote the new binary to the Go default GOBIN (~/go/bin) instead — while the running binary had already been renamed aside. The result: “Updated to vX” followed by grit: No such file or directory. The updater now sets GOBIN to the directory grit actually lives in, so the refreshed binary lands exactly where your PATH expects it. If you hit this on an older version, recover with curl -fsSL https://gritframework.dev/install.sh | sh (or the PowerShell one-liner), then update as normal.

v3.31.51July 1, 2026

Windows dev fix. grit start no longer fails to boot the frontend on machines where Turborepo's native binary can't load.

Dev no longer depends on the turbo binary

On some Windows setups turbo dev exits with 0xC0000135 (STATUS_DLL_NOT_FOUND) because turbo ships a platform-specific native binary that needs the Visual C++ runtime — which blocked grit start on a fresh machine. The scaffolded root dev script now uses pnpm's own parallel runner (pnpm --parallel --filter "./apps/*" --if-present run dev), which needs no native binary, so the web and admin dev servers always come up. Turbo is kept for build / lint / test where its caching helps.

v3.31.50June 26, 2026

Form-sharing polish. Four fixes from one fresh-project test session, all on the /system/form-shares surface.

1. Resource is now a dropdown, not a text input

Typing Catgeory instead of Category in the New Share modal silently created a broken share -- the dispatcher fell through to default, the public form showed an empty state, and the operator didn't find out until a customer hit it. The modal now lists every registered resource in a <select>, sourced from a new GET /api/admin/form-shares/resources endpoint.

2. Form preview with per-field hide toggles

Once a resource is picked, the modal renders a preview of the public form: every field with its type, required/optional badge, and a Hide checkbox for each optional one. Required fields can't be hidden (the submit would 422). The selected hidden keys persist with the share and the public-form endpoint filters them out server-side -- so anonymous visitors never see a column the operator marked private.

3. Custom title + description on the public form

The public form's heading used to be <resource> submission + a hardcoded “Fill out the form below to submit a new <resource>.” Both can now be customised per share in the New Share modal. Title falls back through three sources -- custom_title → label → resource name -- so old shares keep working with the same heading they had before.

4. The public form actually renders the right fields now

This was the big one. v3.31.43 fixed services/form_share_dispatch.go to return the resource's real field schema (via reflection) -- but the matching change to webPublicFormPage() in the framework scaffold never landed. Every project scaffolded with v3.31.43 through v3.31.49 was shipping a hardcoded name / email / phone / message contact form, regardless of what the resource actually looked like.

The scaffold's public form page is now the fields-aware version: reads the new fields[] , custom_title, custom_description from the API and renders one input per field with the right HTML shape (text / email / tel / textarea / number / checkbox / date / datetime / file).

For projects already scaffolded with the stale page, the upgrade is a one-file copy: apps/web/app/forms/[token]/page.tsx from a fresh scaffold replaces the broken one.

Plus: Edit modal now scaffolds in fresh projects

v3.31.43 added an Edit button to the form-shares table -- but only as a hand-applied patch to the ecom test project, never in the scaffold. v3.31.50 ships it properly: a Pencil button on each row opens an Edit modal with the same preview + hide toggles + title / description / password controls.

Data model

models.FormShare gains three columns:

  • custom_title / custom_description -- short strings
  • hidden_fields -- JSON array of field keys to omit

GORM AutoMigrate adds them on next boot. The new // grit:form-share:registered marker in RegisteredResources() gets injected by the generator on each grit generate resource; pre-v3.31.50 projects warn instead of failing.

v3.31.49June 25, 2026

Three small ergonomic wins from real operator feedback. All three landed together in v3.31.49.

1. Activity log shows the operator's real IP, not "::1"

Local-dev activity rows showed ::1 in the IP column because gin's ClientIP() correctly reports the IPv6 loopback for same-machine traffic. Operators expect to see their actual public IP.

The admin / web axios clients now fetch the operator's public IP once per session (cached in sessionStorage; sourced from api.ipify.org) and attach it as X-Public-IP-Hint on every API call. The new services.ResolveClientIP helper honours the hint only when the TCP peer is loopback, so production traffic from real proxies (which sets X-Forwarded-For for gin to consume) keeps using the trusted path and can't be spoofed by a client header.

When the lookup fails (offline / ad-blocker), the feed falls back to the prior behaviour and renders localhost (::1) with the raw value tucked next to it so the origin stays inspectable.

2. Web navbar gets an Admin CTA back

v3.31.42 replaced the navbar's Admin link with the v3.31.42 UserMenu (Login / Sign up + avatar dropdown). Operators landing on the marketing site lost the one-click bounce to the admin app and had to type the URL by hand.

v3.31.49 puts an Admin button back in the navbar, both in the base scaffold (no-auth, post v3.31.48) and in the auth-aware variant. Points at NEXT_PUBLIC_ADMIN_URL (defaults to http://localhost:3001 for dev; set to your prod admin origin before shipping).

3. Landing page surfaces all the dev URLs

The grit new welcome banner prints every URL the scaffold ships with: API, API Docs, GORM Studio, Sentinel, Pulse, Admin, MinIO, Mailhog. Once the terminal scrolls past, operators have to dig back through history to find the right one.

A new <DevLinks /> component renders all of them as a clickable grid at the bottom of the web landing page, grouped by function (App / API / Data / Ops) and colour- coded. The whole section is wrapped in a NODE_ENV !== "production" check at module level so production marketing pages never leak the internal port map -- the section disappears from the prod bundle entirely, not just hidden behind a class.

Files changed

  • Backend: new services/clientip.go (ResolveClientIP); inline mirror in middleware/activity.go (HTTP audit logger); CORS Access-Control-Allow-Headers extended to allow the hint.
  • Admin: lib/api-client.ts fetches + caches the public IP, attaches the hint; activity page's prettyIP helper renders "localhost" for loopback so fall-back rows still read cleanly.
  • Web: components/navbar.tsx + auth variant gain the Admin button; components/dev-links.tsx new file; app/page.tsx renders <DevLinks /> at the bottom.
  • Env: NEXT_PUBLIC_ADMIN_URL documented in .env with the default + a prod-deployment note.
v3.31.48June 25, 2026

Two bug fixes from real user feedback on v3.31.47. Both happened on fresh grit new scaffolds: a broken Go file from a marker collision, and web auth shipping by default when it should be opt-in.

1. injectBefore now matches marker as a standalone line

The generator's injectBefore did a raw strings.Index on the marker. Markers like // grit:form-share:fields sometimes appear inside the docstring of the function they precede (“...at the marker comment...”). The substring match landed there first and the case got injected into the comment, not the function body -- producing a syntax error.

The matcher now walks line-by-line and requires the trimmed line content to equal the marker exactly. Docstrings that mention the marker by name are safe again, and the form-share scaffolded comments stay readable.

2. Web auth is now opt-in via grit add web-auth

The base web scaffold has been quietly shipping the full auth surface since v3.28.1: login / register / forgot-password / callback pages, the five themed AuthShells, the useAuth hook, AuthProvider, UserMenu, the web-session marker, and Login / Sign up buttons in the navbar. That was always intended to be opt-in via grit add web-auth -- the base scaffold should be a clean marketing site with no auth UI.

v3.31.48 moves all of those files into grit add web-auth:

  • Base scaffold: navbar shows Home / Blog / Docs / GitHub only, no Login / Sign up. AppChrome keeps /forms/<token> as the only chromeless prefix (for public form-share).
  • grit add web-auth now writes everything: hooks/use-auth.ts, lib/auth-provider.tsx, lib/web-session.ts, the four (auth) pages, the five themed shells, UserMenu, middleware.ts, ProtectedWebRoute.tsx -- and REPLACES the navbar + AppChrome with their auth-aware variants (which add the (auth) chromeless prefixes and the UserMenu in the navbar). Replacement requires --force for safety.

Migrating existing projects

Projects scaffolded with v3.31.x before this release already have the auth files. They keep working unchanged -- no removal happens automatically. Future grit new calls produce the clean base scaffold; if you need auth on a fresh project, run grit add web-auth right after grit new.

Both bugs were reported same day

The user spun up a fresh ecom-app, hit the form-share dispatch syntax error ongrit dev, then noticed the web shipping auth they didn't want. Both shipped fixed in v3.31.48 within a few hours.

v3.31.47June 25, 2026

The Preset Chart builder. Operators can now build custom charts straight from Dashboard Settings -- pick a resource, pick a preset, pick a visualization. The charts render in the Charts section alongside the system Activity + Severity widgets. No SQL involved.

Four presets

The presets cover the bulk of admin-dashboard needs without introducing a query plane:

  • Count over time -- daily count of new records (no field needed)
  • Group by field -- top-N counts grouped by a categorical column (e.g. orders by status, products by category)
  • Sum over time -- daily sum of a numeric column (e.g. revenue per day)
  • Avg over time -- daily average of a numeric column (e.g. average order value)

Five visualizations

Each chart renders as bar, line, area, pie, or donut -- using Recharts. The builder dims out incompatible combinations (pie for a time-series, line for group_by) so users see why a choice doesn't make sense rather than picking a broken combo and getting a flat chart.

How it works under the hood

Same dispatch pattern as the v3.31.44 resource stats. A new service file chart_dispatch.go ships with a switch over resource name + a reflective helper that runs the right SQL for each preset:

  • count_over_time: pulls timestamps + buckets in-memory (portable across SQLite + Postgres)
  • group_by: SQL GROUP BY field ORDER BY COUNT(*) DESC LIMIT N
  • sum_over_time / avg_over_time: pulls (created_at, field) pairs + aggregates in-memory

Field whitelisting is the security boundary: the helper reflects on the model to build two sets (string/bool columns valid for group_by, numeric columns valid for sum/avg) and rejects any field not in the right set. The same dispatch marker used by v3.31.44 (// grit:resource-stats:dispatch) is reused, so one generator injection covers both the sparkline + the chart presets for a new resource.

New endpoint

GET /api/admin/dashboard/chart/:resource?preset=group_by&field=status&limit=10 returns { data: { preset, rows: [{x, y}], meta } }. The frontend ChartCard renders the right Recharts component based on the saved viz; the {x, y} shape works for all four presets without a discriminator.

Data model

models.DashboardLayout gains one new JSON column:

  • custom_charts -- array of user-defined chart configs. The PUT handler validates each entry on write (drops malformed rows individually rather than rejecting the whole save).

GORM AutoMigrate adds the column on next boot. No manual migration; existing saved layouts continue to work (empty array = no custom charts).

Frontend pieces

Three new files in the admin scaffold:

  • components/dashboard/CustomChartCard.tsx -- renders one chart with Recharts. Loading + error states inline so a broken chart never blanks the section.
  • components/dashboard/ChartBuilderForm.tsx -- the inline builder. Resource picker, preset picker (with tiles), field picker (filtered by preset), viz picker (with grey-out for incompatible). Used in the new Custom charts section on Dashboard Settings.
  • Settings page Custom charts panel -- lists saved charts with Edit + Delete; click Add chart to open the inline builder.

Research note

The design is the “Preset Charts” pattern from the research pass (Metabase, Grafana, Looker Studio, Superset, Power BI). It's the table-first pattern with curated presets rather than the dimension/metric drag-drop pattern -- fits Grit's convention-over-configuration audience and slots straight into the v3.31.45 settings page. The dimension/metric builder (Design B from the research) stays available as a future v3.32 upgrade if users start asking for one more axis of freedom.

v3.31.46June 25, 2026

Polished By-Resource Latest tables + per-resource layout toggle. The v3.31.44 Latest list rendered a single “Name: X · Status: Y” line per row, which turned any FileRef column into a JSON blob visible to the user. v3.31.46 swaps that for the same column-driven table layout the resource list page uses, with proper FileRef thumbnails, badges, date formatting, and currency rendering.

The Latest table now uses renderCell

Every cell in the dashboard's Latest table now goes through the same renderCell dispatch the resource list pages already use. That means columns with format: "image" render thumbnails, columns with format: "badge" render the configured pill, dates and currency get their normal formatting. The column picker still uses the v3.31.44 heuristics (prefer name/title/email/status/price) but now always reserves a slot for any image / FileRef column so visual rows always have a thumbnail when the model defines one.

Per-resource layout toggle: Split vs Tabs

The v3.31.44 layout was hardcoded: Split (Total card ~33% on the left, Latest table ~67% on the right). That ratio breaks down for resources with many columns or long string values -- the Latest table never has room to breathe. v3.31.46 adds a per-resource layout mode:

  • Split (default) -- the v3.31.44 side-by-side layout.
  • Tabs -- both widgets render full-width inside a tabbed container. Two tabs (Total <Resource> / Latest <Resource>) with the Latest tab opened by default since that's the widget that benefits most from the extra width.

Picked per resource in Dashboard Settings under the new Resource layout panel (below the By Resource checkboxes). Only resources with at least one widget enabled show up in the picker -- the choice is moot otherwise.

Data model

models.DashboardLayout gains one new JSON column:

  • resource_layouts -- a string-keyed object ({ "products": "tabs", "orders": "split" }). Only non-default (tabs) entries are persisted; missing slugs fall back to split at render time. The PUT handler validates incoming values and silently drops anything that isn't split or tabs.

GORM AutoMigrate adds the column on next boot. No manual migration; existing saved layouts continue to work (an empty map means “every resource uses split” -- the v3.31.44 behaviour).

Coming in v3.31.47

Next release will tackle the “build a custom chart” ask -- give operators a way to pick a resource + group-by field + aggregation + visualization (bar/line/pie/donut) without writing SQL. The design landed on the “Preset Charts” pattern (count over time, group by field, sum/avg over time, top-N) -- those four cover the bulk of admin-dashboard needs without introducing a query plane, and slot straight into the same Dashboard Settings page as the v3.31.45 toggles.

v3.31.45June 25, 2026

Per-resource dashboard customisation + section reordering. The v3.31.44 “By Resource” band was uncustomisable — it always rendered the Total + Latest pair for every resource. Dashboard Settings now exposes both halves per resource, and the four top-level dashboard sections (Cards, Charts, Tables, By Resource) can be reordered.

Per-resource toggles in Dashboard Settings

A new By Resource section appears at the bottom of /settings/dashboard, grouped by resource. Each resource exposes two checkboxes:

  • Total <Resource> — the stat card with the 30-day sparkline.
  • Latest <Resource> — the newest-N records table.

Toggling either one off hides just that widget; the row stretches the visible half to fill the available width. Resources with both halves unchecked don't render at all. The resource-level dashboard: { enabled: false } opt-out still exists for resources that should never appear on the dashboard, even as catalog entries.

Section reorder

A new Section order panel sits at the top of Dashboard Settings, showing the four sections as a numbered list with up/down chevrons. The saved order persists on the existing DashboardLayout row (new section_order column). The dashboard page renders the sections in that order using CSS order on a flex container — no JSX restructure was needed.

Data model — two new JSON columns

models.DashboardLayout gains two fields, both JSON arrays:

  • resources — enabled keys for the By Resource band, formatted as "<slug>:total" / "<slug>:latest". Same presence-vs-absence semantics as the existing cards / charts / tables arrays: an empty list on a saved row means “hide everything”; a missing row means “show defaults.”
  • section_order — section keys in render order. Default empty (= built-in order). Unknown keys are silently dropped at render time; missing default keys get appended to the end so a saved layout from before a new section was added still renders the new section.

Backward compatibility

Pre-v3.31.45 projects don't have the new columns. GORM AutoMigrate adds them on next boot. Existing saved layouts continue to work — both new arrays default to empty, which means “use built-in defaults” (all resource widgets shown, default section order). Frontend SavedLayout gains the two fields as required (TypeScript-side); the wire shape allows them to be omitted on the PUT body, treated as empty.

v3.31.44June 25, 2026

Per-resource dashboard widgets, scoped by DateFilter. Every newly generated resource now ships with two preset widgets on the main dashboard: a Total stat card with a 30-day sparkline on the left and a Latest 5 records preview on the right. Both honor the existing dashboard DateFilter so the count obeys whichever range the operator has selected.

How it works

Three pieces ship together:

  • Service: services.ComputeResourceStats in apps/api/internal/services/resource_stats_dispatch.go — a generator-driven switch over resource name, each case calls a single reflective helper that counts rows in the active range, builds a 30-day sparkline, and lists the newest N (JSON round-tripped so json:"-" columns like PasswordHash never leak).
  • Endpoint: GET /api/admin/dashboard/resource-stats/:resource — accepts the same created_since / created_from / created_to params the resource list pages already use, so the wire shape matches.
  • Widgets: ResourceStatCard, ResourceLatestTable, and a thin ResourceWidgetsRow wrapper. The dashboard page maps over registered resources and renders one row per resource below the existing Quick Access section.

Sparkline window is always 30 days

The sparkline ignores the active date filter on purpose — under the “Today” preset it would collapse to a single bar, which carries no information. The total + latest list still obey the filter; only the trend chart is fixed.

Opt-out per resource

Resources can hide their widgets by setting dashboard: { enabled: false } in the resource definition. The flag is opt-out by design: a new resource is more often than not worth showing on the dashboard.

Backward compatibility

The generator injects a switch case into resource_stats_dispatch.go on each grit generate run, at the marker // grit:resource-stats:dispatch. Projects scaffolded before v3.31.44 don't have the file or the marker — the generator detects this and prints a one-line warning instead of failing. Patch existing projects by copying the scaffold file from the framework repo, then re-running grit generate to populate the cases (or hand-edit them).

v3.31.43June 25, 2026

Form-share polish: matching public form + editable shares. Two small but high-impact fixes on top of the v3.31.41 form-share generator. Both ship through the framework scaffold and the generator, so new resources pick them up automatically and existing projects get the imports added lazily on the next grit generate.

1. Public form renders the resource's actual fields

Before this release the public share page at /forms/[token] rendered a hardcoded Name + Email + Phone + Message contact form for every resource. Creating a share for a Category with name + image fields still showed the contact form — and the Name field happened to line up purely by coincidence. Submitting any other field shape was effectively impossible.

The dispatcher now exports services.PublicFields(resourceName), a per-resource switch that reflects the model struct and returns a typed PublicFieldInfo[] with one entry per user-facing column (framework + auto fields are skipped). The HTTP type for each field is inferred from the Go type: FileRef → file, time.Time → datetime, bool → checkbox, int/float → number, and string with name heuristics for email / phone / textarea fields.

GET /api/public/forms/:token now returns a fields[] array alongside resource_name and has_password. The web page consumes it and renders one input per field, with proper shapes for checkbox, number, textarea, and date/datetime. File fields render an inline “File uploads aren't supported on public-share forms” explainer instead of an unusable input — file uploads require the auth-gated /api/uploads endpoint and aren't supported on anonymous shares yet.

2. Admin can edit existing shares

The admin form-shares page already had Audit / Copy / Open / Delete buttons but no way to change a share's label or password protection after creation. Want to add a password to an existing link? Delete + recreate, and re-distribute the new token to every recipient.

A new Edit button opens a modal with three controls:

  • Label — free text, optional.
  • Password mode — three pills: Keep current, Set password, Remove password. “Remove” is disabled when the share has no password.
  • New password — shown only when mode is “Set password”.

The backend handler at PATCH /api/admin/form-shares/:id already supported the full payload (it accepts password: "-" as the sentinel for “remove”); this release just adds the missing UI to call it.

Backward compatibility

Projects scaffolded before v3.31.43 don't have the // grit:form-share:fields marker or the reflect + strings imports the new code depends on. The generator now adds the imports lazily on the first generated resource and prints a one-line warning when the marker is missing, pointing operators at a manual patch. Existing shares keep working — they just continue to show the hardcoded form until the project is re-scaffolded or the dispatcher is patched.

v3.31.42June 25, 2026

Three web-auth fixes shipped together. All surfaces — admin scaffold, web scaffold, grit add web-auth, and the generator — picked up the changes so existing and new projects both benefit.

1. Auth pages render full-bleed (no navbar / footer)

Until now the web app's (auth)/login, (auth)/register, (auth)/forgot-password, (auth)/callback, and forms/[token] pages all rendered inside the root layout, which pinned the <Navbar /> + <Footer /> to the top and bottom. The auth pages already supply their own AuthShell chrome (same template the admin uses) so the result was visually doubled.

New components/AppChrome.tsx is a tiny client wrapper that conditionally renders Navbar + Footer based on the pathname. The root layout drops to a thin server component again. Auth + public form-share pages render full-bleed; everything else is unchanged.

2. grit_web_session marker stops admin sessions from unlocking web pages

grit_access is set by the API on the API origin (localhost:8080). That same cookie is also used by apps/admin — so an operator who signed in via the admin app could open apps/web/account in the same browser and walk straight past the web middleware. The API call from useMe() succeeded (the cookie is valid), and ProtectedWebRoute happily rendered the page.

New grit_web_session marker cookie is set by the web app's own login/register flow on the WEB origin (localhost:3000) via client JS — non-HttpOnly, intentionally — and cleared by logout. Middleware reads grit_web_session instead of grit_access. Admin-only sessions never stamp the marker, so the web gates bounce them. The real session security is unchanged: useMe() still validates the API JWT; the marker is just a fast edge check.

Mechanically: lib/web-session.ts with setWebSessionMarker / clearWebSessionMarker / hasWebSessionMarker; called from useLogin + useRegister (onSuccess), useLogout (onSettled), and the direct-submit (auth)/login + (auth)/register pages.

3. Navbar UserMenu replaces the Admin button

The web navbar used to ship with a single Admin link that punted everyone to the admin app's login. Replaced with components/UserMenu.tsx:

  • Signed outLog in + Sign up buttons that link to the web app's own auth flow.
  • Signed in — avatar dropdown with name + email at top, Account link, Sign out button.
  • Loading — a placeholder of the same width as the signed-out CTA pair so the navbar doesn't shift on useMe() resolve.

Bonus: web-hook generator now imports FileRef

Same TS2304 fix shipped in v3.31.37 for writeTSTypes, applied to writeReactQueryHooks. Generated apps/web/hooks/use-X.ts files with :file: / :files: fields now emit import type { FileRef } from "@repo/shared/schemas" at the top.

Migration

Run grit upgrade or hand-patch:

  • Drop apps/web/components/AppChrome.tsx + UserMenu.tsx + lib/web-session.ts in.
  • Update app/layout.tsx to render <AppChrome> instead of <Navbar /> ... <Footer />.
  • Update middleware.ts to read grit_web_session.
  • Add the marker calls to hooks/use-auth.ts + (auth)/login/page.tsx + (auth)/register/page.tsx.
  • Replace the Admin button in components/navbar.tsx with <UserMenu />.
v3.31.41June 25, 2026

System Hub tile for Public form sharing + two course lessons rewritten to match what actually happens. No code-behavior changes — this release closes the gap between the documentation and the running system.

System Hub tile

The /system/form-shares page has existed since v3.31.20 but wasn't reachable from the /system tile grid — operators had to know the URL by heart. The course lesson pointed users at "System → Public form sharing" which only existed as a sidebar shortcut, not as a Hub tile. Now both surfaces carry it. The tile shows up in all four System Hub variants (default admin_v331_files.go + minimal/modern/glass via admin_v3315_pages.go).

public-form-sharing lesson

Fixed the navigation reference (System Hub tile vs direct route) and added a new section on file upload limitations. The default /forms/[token] template renders text inputs only — :file: / :files: / :image: columns are silently skipped because /api/uploads is auth-gated. The lesson now walks through three production-shaped workarounds (presigned URLs, external links, magic-link auth).

grit-expose lesson

Added a deep "Behind the scenes: how the auth bypass actually works" section that walks the four security layers operators should understand before shipping a --public-share form to production:

  • The routes live in a separate Gin group (publicForms := r.Group("/api/public/forms")) with no middleware.Auth.
  • Sentinel rate-limits each token aggressively by IP.
  • The dispatcher service is the real security boundary — only resources with an explicit case in the switch can be created; unknown keys in the request body are silently dropped at the typed-struct decode.
  • The optional bcrypt password on the FormShare row is the fourth layer.

Plus the same file-upload caveat with a copy of the three workaround paths.

Migration

Run grit upgrade to pull the System Hub tile into an existing project. The tile is purely additive — no behaviour change for anything that was already working.

v3.31.40June 25, 2026

Per-user dashboard customisation + dashboard date filter. A new Settings → Dashboard page lets each operator pick which stat cards, charts, and tables show up on their dashboard, grouped by module. The dashboard also gets a date-window filter that scopes every stat and chart to the selected range.

Backend

  • New model DashboardLayout (one row per user, unique on user_id) with three JSONSlice[string] columns for cards / charts / tables plus a date_preset text column for the persisted window.
  • New handler exposing GET /api/dashboard-layout (returns the current user's saved layout or a zero-valued struct if none) and PUT /api/dashboard-layout (whole-row replace).
  • Empty layout (id === "") = "show all widgets". Saved layout with cards: [] = "hide every stat card". The frontend distinguishes the two by checking layout.id.

Widget catalog

New lib/dashboard-catalog.ts aggregates the catalog of pickable widgets from two sources:

  • System widgets (Users, Events 24h, Notifications, Resources count, Activity 7-day chart, Severity mix, Recent activity, Quick access) — these are the legacy hard-coded dashboard tiles, now opt-out-able per user.
  • Per-resource widgets — every entry in a ResourceDefinition.dashboard.widgets array contributes one catalog entry, grouped under the resource's module name.

Widget keys are stable strings (system:users, products:total-products, etc.) so the saved layout doesn't break when widget order changes in the definition.

Settings page

At /settings/dashboard: three sections (Cards / Charts / Tables), each with checkbox lists grouped by module. Per-section Select all / Deselect all; per-module All / None for fine-grained configuration. Sidebar nav gets a new "Dashboard settings" entry under System (no admin gate; every user can customise their own view).

Dashboard date filter

The existing DateFilter component from v3.31.34 is now on the dashboard too. URL-persisted via ?date=preset / ?date_from / ?date_to; initial value falls back to the saved date_preset so a refresh keeps the window. Every system widget query keys on the active dateParams so changing the filter retriggers a refetch with ?created_since=7d / ?created_from=...&created_to=... appended.

Migration

New scaffolded projects ship everything wired up. To add to an existing project, run grit upgrade (which writes the new files), then hand-add the model to models/user.go's Models() list and the routes to routes.go — or rerun the upgrade with --force if you haven't customised those files. The existing hand-coded dashboard page keeps working with no changes; the new filtering activates only after you replaceapp/(dashboard)/dashboard/page.tsx with the v3.31.40 template (it reads useDashboardLayout() and resolveEnabledKeys()).

Heads-up for early adopters: the v3.31.40 framework scaffold ships the building blocks (model + handler + catalog + hook + Settings page + sidebar entry) but does not auto-rewrite the dashboard page — that lands in a follow-up release once the multi-style dashboard variants (default / modern / minimal / glass) are all refactored to use the new layout reader. Until then, the example dashboard refactor in the docs walks you through the changes by hand.

v3.31.39June 24, 2026

CUD activity logging on every generated resource. Until this release the Activity feed only carried sign-ins and sign-outs — every Create / Update / Delete on a generated resource went unrecorded. Now each one writes a row with a human-readable summary using a fixed format convention.

Format convention

{verb} {entityType} {identifier}[: {detail}]

identifier is the human-readable label (name, title, slug, sku); it must never be blank — the helper falls back to (unnamed) if the caller hands it an empty string. detailis optional extra context (price for Create, diff for Update) and only renders when non-empty.

Example rows in the feed:

  • Created Product Desktop: KES 340,000
  • Updated Product Desktop: changed name, price
  • Updated Category Phones: image changed
  • Deleted Blog "Welcome to the new site"

Three new helpers in services/activity.go

  • LogCreate(db, c, entityType, identifier, resourceID, detail)
  • LogUpdate(db, c, entityType, identifier, resourceID, detail)
  • LogDelete(db, c, entityType, identifier, resourceID)

Plus DiffSummary(updates) for rendering a GORM Updates() map as a sorted, deterministic diff string (1 field → field changed; 2–3 fields → changed a, b, c; 4+ → N fields changed (a, b, c, ...)). Errors are logged, never returned — losing an audit row should not fail a real request.

Generator emits the calls automatically

Every grit generate resource from v3.31.39 onward inserts:

  • services.LogCreate(...) after a successful Create
  • services.LogUpdate(...) after Update and Patch (the grouped Save handler from v3.31.18 is logged the same way)
  • services.LogDelete(...) after Delete

Identifier expression is picked at generation time from the model's fields, in priority order: Name, Title, Slug, SKU, Subject, Label, Email. Falls back to item.ID so the log line is never blank.

Migration

Re-run grit generate resource X for each existing resource (the file rewrite picks up the new log calls), or hand-patch each handler:

  • Add "<module>/internal/services" to the imports.
  • Drop services.LogCreate/Update/Delete(...) calls right after each success path, before thec.JSON(...).
  • Use services.DiffSummary(updates) for the diff string in Update / Patch.

Existing auth helpers (LogLogin, LogRegister, LogLogout) are unchanged — they keep their semanticauth.X action names.

v3.31.38June 24, 2026

Auto comma-formatting on number inputs. Typing 3000 in a price field now reads 3,000 on screen the moment the fourth digit lands. Helps catch zero-count mistakes (the "is that 30k or 300k?" problem) without changing the wire shape — the form still submits a plain JS number, and the API still receives an int or float as before.

How it works

NumberField switched from type="number" to type="text" with inputMode="decimal" (or "numeric" for int/uint columns). The visible value is the comma-formatted string; the field also keeps a parsed JS number in form state. Mobile keyboards still pop up correctly thanks to inputMode; the comma can render literally because text inputs don't strip non-digits.

Cursor position is preserved across the reformat by counting non-comma characters before the caret and restoring after the same count in the new value, so editing in the middle of a number doesn't fling the caret to the end.

numberKind hint

New FieldDefinition knob:

numberKind?: "int" | "uint" | "float"

Tells the input which characters to accept:

  • int — negatives yes, decimals no
  • uint — neither negatives nor decimals
  • float — both (legacy permissive default when unset)

The generator now emits the right numberKind for every number field based on the Go column type. grit sync also adds it when injecting newly-added Go fields into existing admin resource files. Hand-written resources that don't set numberKind stay permissive — no breaking change to your existing forms.

Edge cases handled

  • Paste "$1,234.56" — strips the dollar, keeps the value
  • Mid-typing "3000." — preserves the trailing dot so the user can finish the decimal
  • Backspace through a comma — the comma re-inserts after the digit is removed; caret tracks digit count, not column position
  • Leading zeros — "0123" collapses to "123"; "0" stays "0" so "0.5" is reachable
  • External value sync — opening Edit on an existing record formats the loaded number; subsequent typing skips the sync to avoid stomping mid-edit state

Migration

Replace apps/admin/components/forms/fields/number-field.tsx with the rewritten file and add the optional numberKind knob to apps/admin/lib/resource.ts on FieldDefinition. Existing resource files keep working — numberKind defaults to float when unset, which gives the legacy permissive behaviour. Run grit sync on any project to backfillnumberKind for new fields the generator finds; existing fields aren't touched.

v3.31.37June 24, 2026

Bug fix: opening a Create form on a resource with a :files: column no longer crashes into the global error boundary. Same release also tidies up three companion TS errors that were red-squiggling in IDEs even though SWC stripped them at runtime.

What was broken

buildDefaults in form-builder.tsx seeded every non-toggle field to "" (empty string). For files / images / videos types, react-hook-form's initial state was therefore a string. The matching field component (FilesField, ImagesField, VideosField) immediately called .map() on that "array" — strings have no .map → TypeError → the parent FormSheet blew up into Next.js' error boundary. The user saw "Something went wrong".

The fix

buildDefaults now branches by field type: arrays default to [], nullable objects (file / image / video) default to null, toggles stay false, everything else stays "".

const ARRAY_FIELD_TYPES = new Set([
  "files", "images", "videos", "multi-relationship-select",
]);
const NULLABLE_OBJECT_FIELD_TYPES = new Set([
  "file", "image", "video",
]);
// ...
} else if (ARRAY_FIELD_TYPES.has(field.type)) {
  defaults[field.key] = [];
} else if (NULLABLE_OBJECT_FIELD_TYPES.has(field.type)) {
  defaults[field.key] = null;
}

All three field components also got a defensive Array.isArray guard so a stale form or a deserialised-wrong API response can't crash the dropzone — the field just renders empty and the user can still upload.

TypeScript clean-up

  • ColumnFormat union now includes "file" and "files" — both renderers were already implemented but the type union was stale, so resource definitions emitted by the v3.31.30+ generator flagged a TS2322 on every file column.
  • Generated packages/shared/types/<model>.ts files now import FileRef from schemas/file-ref when any field is :file: or :files: — previously the type was referenced without an import, fine at runtime but red squiggles in the IDE.
  • ImportModal's narrowing check against resource.table.import dropped the redundant !== false comparison (TS2367) — a plain truthy check correctly handles all three values of the union.

Migration

Run grit upgrade, or hand-patch:

  • apps/admin/components/forms/form-builder.tsx — update buildDefaults + the file / files renderer fallbacks.
  • apps/admin/components/forms/fields/{files,images,videos}-field.tsx — replace the (value ?? []).map() with the Array.isArray guard.
  • apps/admin/lib/resource.ts — add "file" and "files" to ColumnFormat.
  • apps/admin/components/tables/import-modal.tsx — simplify the importCfg check.
  • For models with file columns, add import type { FileRef } from "../schemas/file-ref"; at the top of packages/shared/types/<model>.ts.
v3.31.36June 24, 2026

Bug fix: FileRef inserts now succeed on Postgres. Single-file (:file:) and multi-file (:files:) columns failed to insert on Postgres with ERROR: invalid input syntax for type json (SQLSTATE 22P02). SQLite and MySQL projects weren't affected. This release fixes the framework scaffold; existing projects get a one-file patch.

What was broken

FileRef.Value() and FileRefs.Value() returned the []byte from json.Marshal() directly:

func (f FileRef) Value() (driver.Value, error) {
  return json.Marshal(f)   // returns []byte
}

Go's database/sql accepts []byte as a valid driver.Value — and lib/pq (the standard Postgres driver) encodes []byte as bytea, Postgres' binary type. Postgres then tries to insert the bytea blob into a json column, fails to parse the framing, and rejects with SQLSTATE 22P02.

The fix

Both Value() implementations now convert the JSON bytes to a Go string before returning:

func (f FileRef) Value() (driver.Value, error) {
  b, err := json.Marshal(f)
  if err != nil {
    return nil, err
  }
  return string(b), nil   // text, not bytea
}

lib/pq sends string values as plain text, which Postgres parses as JSON cleanly. SQLite and MySQL are tolerant of both shapes; only Postgres was strict.

Regression guards

Two new tests in the scaffolded file_ref_test.go assert that Value() returns a string type — so a future contributor can't silently revert to []byte without CI catching it. The tests fail with a clear message pointing at the Postgres bytea-vs-json issue.

Migration

Replace apps/api/internal/files/file_ref.go with the regenerated copy:

grit upgrade --files

Or hand-patch both Value() methods to wrap the json.Marshal result in string(b) before returning. Postgres-on-prod users running existing projects should ship this immediately. SQLite-dev or MySQL projects have no urgency.

v3.31.35June 24, 2026

Excel import + export, fully client-side via SheetJS. Continues the data ops arc. Every resource list page now ships with a three-format download menu (CSV / Excel / JSON) and a drag-and-drop Excel import that previews, validates, and submits rows without a single new API route.

Why client-side

The original v3.31.35 plan put export and import on the server: excelize for writing, asynq + Resend for the >5000-row async cutoff, a new /import endpoint with template + validation. Doing it in the browser via SheetJS (xlsx ^0.18.5) collapses all of that — no new routes, no async wiring, no "your file is ready" email loop, and tenant row data never leaves the user's session just to build a file.

Trade-off: very large datasets (~50k+ rows) are gated by the browser's memory ceiling, not server RAM. The export menu still streams every page from the API before building the file, so the output represents the whole filtered dataset — not just what's on screen.

New lib/excel-utils.ts

  • exportToFile(rows, columns, name, format) — writes CSV / XLSX / JSON, auto-sizing columns up to 60 chars.
  • fetchAllPages(endpoint, params, onProgress) — loops the resource API at page_size=200 until every row is in hand.
  • downloadImportTemplate(resource, allowedFields?) — blank workbook keyed by form field keys with a placeholder example row.
  • parseImportFile(file, resource, allowedFields?) — coerces each cell to the right JS type via the field definition, returns per-row errors.
  • submitImport(endpoint, rows, onProgress) — POSTs each valid row at concurrency 4 with live progress.

ExportMenu

Split button in the toolbar: clicking the main half exports in the default format (Excel when enabled, else CSV); the chevron opens a menu with the other formats. Uses the active search, sort, filters, and date range so an export honours the view the user is looking at.

ImportModal

Three stages — file pick → validation preview → submit with progress bar. The preview surfaces per-row errors with field+reason, flags unknown header columns, and disables Import when nothing is valid. On submit, React Query invalidates the resource list so the table reflects the new rows.

Header matching is loose: spaces, underscores, hyphens, and case are normalised before lookup, so the same template works whether a user's spreadsheet has first_name, First Name, or firstname.

Per-resource opt-out

table: {
  // Hide a format from the export menu (default: all on).
  export: { csv: true, excel: true, json: false },
  // Or disable export entirely.
  // export: false,

  // Restrict importable fields to a subset.
  import: { fields: ['title', 'price', 'stock'] },
  // Or disable import entirely.
  // import: false,
}

Migration

Three new files in your scaffolded admin app: apps/admin/lib/excel-utils.ts, apps/admin/components/tables/export-menu.tsx, apps/admin/components/tables/import-modal.tsx. Three refreshed files: apps/admin/lib/resource.ts, apps/admin/components/tables/table-toolbar.tsx, apps/admin/components/resource/resource-page.tsx. Run grit upgrade to pull them in. The xlsx dependency was already declared in package.json from v3.31.34, so no install step is needed.

Coming next

v3.31.36: PDF export via @react-pdf/renderer.

v3.31.34June 24, 2026

Date filter end-to-end + stats now actually reflect the filtered window. Begins the data ops arc and fixes a latent bug where "This Week" / "This Month" stat cards were showing the total count instead of the windowed count.

The latent stats bug

Auto-default stat cards have been emitting endpoints like /api/products?page_size=1&created_since=7d for a while, but the API ignored created_since — so the "This Week" card returned the same total as the "Total" card. v3.31.34 makes the backend honour the param.

Server-side (paginate package)

Bind(c) now parses four query params:

  • ?created_from=2026-01-01 — inclusive lower bound
  • ?created_to=2026-12-31 — inclusive upper (snapped to 23:59:59.999)
  • ?created_since=7d — relative shortcut (h / d / w / m units)
  • ?date_field=published_at — override the default created_at target column

Explicit created_from / created_to win over created_since so a stat-card link doesn't clobber a user's picked range. Applied as a single WHERE clause in List[T]; both offset and cursor pagination paths inherit.

Resource def

table: {
  dateFilter: { enabled: true, field: 'created_at', label: 'Created' }
}

Enabled by default. Set enabled: false to hide. Override field for resources where the meaningful date isn't created_at (e.g. a Booking resource filtering by scheduled_for).

DateFilter component

New <DateFilter> in components/tables/date-filter.tsx:

  • Four presets — Today, Last 7 days, Last 30 days, This month
  • Custom range with two date inputs + Apply button
  • Active state shows the current selection as a toolbar pill; X clears
  • Close-on-outside-click popover
  • URL-persisted via ?date=preset + ?date_from / ?date_to so refresh + shared links rehydrate

Stats reflect the filter

When the user picks a date range, ResourceListView appends the resolved query params to every stat card's endpoint. The card labels stay fixed ("Total", "This Week", etc.) but their numbers now match the table below. No more "Total: 10,000; list shows 142" mismatch.

Migration

Four files refreshed: apps/api/internal/paginate/paginate.go, apps/admin/components/tables/table-toolbar.tsx, apps/admin/components/resource/resource-page.tsx, apps/admin/hooks/use-resource.ts, plus the new apps/admin/components/tables/date-filter.tsx.

Coming next

v3.31.35: Excel import + async cutoff for export (>5000 rows = asynq job + Resend email) + per-resource opt-out. v3.31.36: PDF export via @react-pdf/renderer.

v3.31.33June 24, 2026

File lifecycle — immediate S3 delete on replacement + daily orphan cleanup cron. Closes the loop on the file-fields work from v3.31.30-32: bucket no longer accumulates dead objects when files get swapped or forms get abandoned.

internal/files lifecycle helpers

  • DiffSingle(old, new) — returns the key removed when a single-file column is replaced or cleared.
  • DiffMulti(old, new) — returns keys present in old but missing from new (gallery pruning).
  • CleanupRemoved(ctx, st, old, new) — reflection-based: walks both struct values, finds *FileRef + FileRefs fields, computes the diff, deletes the removed S3 objects. One line in the handler regardless of how many file columns the resource has.
  • ClaimRefs(ctx, db, record) — walks the same FileRef columns and stamps claimed_at = now() on the underlying Upload rows so the orphan cleanup cron knows the upload is in use.
  • RunOrphanCleanup(ctx, db, st, minAge) — finds Upload rows with claimed_at IS NULL older than minAge (24h), deletes them from S3 and the DB. Best-effort S3 delete: if it fails we still drop the DB row so the same orphan isn't retried forever.

Upload.ClaimedAt column

New nullable timestamp on the Upload model. Auto-migration adds it; existing rows start as NULL and get claimed the next time their parent record is updated. The 24h grace period before orphan cleanup means a fresh deploy won't purge historical uploads — the cron only catches uploads truly created in the past 24h that never got claimed.

Daily cron job

New uploads:cleanup_orphans asynq task runs at 03:15 daily (low-traffic window). Registered in internal/cron/cron.go and handled by handleUploadsOrphanCleanup in internal/jobs/jobs.go.

Generated handler injection

Resources with :file: / :files: fields now get:

  • Storage *storage.Storage field on the Handler struct, wired in routes via Storage: svc.Storage.
  • Create handler: files.ClaimRefs call after successful save.
  • Update handler: snapshots the old record, diff-deletes removed S3 objects, then claims the new refs.

Resources without file fields stay exactly as before — no Storage field, no extra imports, no dead code. The injection is conditional on the generator detecting at least one file/files field.

Migration

Existing projects: the Upload model needs the ClaimedAt column. GORM auto-migration in cmd/server/main.go handles it on next boot. Re-run grit generate resource <Name> on any resource with file fields to pick up the cleanup-aware handler template.

Coming next

v3.31.34 begins the data ops arc — server-side Excel export via excelize, bulk Excel import with template generation + row-by-row validation, React-PDF rendering, per-page date filter, and per-resource opt-out for export / import.

v3.31.32June 24, 2026

Storage admin page surfaces FileRef totals. The original Files page was a flat uploads grid — useful for browsing but offered no sense of how much storage you were actually using, or what was eating it.

New API endpoint

GET /api/uploads/stats returns:

  • total_count — how many uploads
  • total_size — sum of bytes
  • by_kind — count + size grouped by MIME bucket (image / video / audio / pdf / document / spreadsheet / other). Single SQL GROUP BY with portable CASE expression — works on Postgres and SQLite without engine-specific JSON functions.

Storage stats panel

The Files admin page now shows three big numbers up top (Total files / Total storage / Avg file size), then a per-kind breakdown with proportional progress bars sorted by largest consumer. Image-heavy projects can see at a glance whether to migrate to a CDN; CSV-heavy projects can spot a runaway export pipeline.

Dropzone variant standardisation

Default + Compact variants now route their uploading state through the unified <UploadProgress> component so the per-field progress prop (bar / circular / pulse) actually takes effect on both. Minimal, Avatar, and Inline variants are space-constrained by design and keep their bespoke single-spinner treatment.

Migration

Three files refreshed: apps/api/internal/handlers/upload.go (Stats handler), apps/api/internal/routes.go (new route), apps/admin/app/(dashboard)/system/files/page.tsx (stats panel) and apps/admin/hooks/use-system.ts (useUploadStats hook). Re-run grit generate resource for any resource to pull the updates.

Coming next

v3.31.33 ships the file lifecycle work: immediate S3 delete when a record swaps its file, plus a daily orphan-cleanup cron that purges Upload rows whose key is referenced nowhere. v3.31.34 begins the data ops arc — date filter, Excel import/export, PDF render via @react-pdf/renderer.

v3.31.31June 24, 2026

File fields polish — progress variants, type-aware previews, reorder.

Bug fix from v3.31.30

The Dropzone was still reading data.original_name and data.mime_type from the upload response, but v3.31.30 changed POST /api/uploads to return a FileRef shape with data.name and data.mime. Files uploaded after v3.31.30 appeared with the generic File client-side fallback name instead of the real filename from the server. Now reads both shapes (FileRef first, legacy fallback) so cross-version compatibility holds.

UploadedFile also carries the explicit key field now, so the FileField bridge round-trips the S3 key losslessly instead of recomputing it from the URL pathname.

Three progress variants

  • bar (default) — linear progress bar with spinner + percentage label.
  • circular — donut with the % inside. SVG, no extra dependency.
  • pulse — three pulsing dots + %. Minimal chrome for compact contexts.

Pick a variant per field:

{ key: "avatar", type: "file", accepts: ["image"], progress: "circular" }

The Default dropzone variant routes its uploading state through the new <UploadProgress> component. The other four dropzone variants (compact / minimal / avatar / inline) keep their bespoke inline progress UI for now — v3.31.32 standardises them.

Type-aware FilePreview

Single image preview stays as a thumbnail. Video gets a play badge over a dark thumb. Audio shows a music icon. PDF / Word / Excel / CSV render format-specific glyphs with colour-coded tints (PDF red, Word blue, Excel green). Everything else falls back to the generic File icon.

Reorder by up/down arrows

Multi-file (:files:) preview rows now show small up/down arrow buttons when reorderable is true (default). Adjacent swap; first row's up button is disabled, last row's down button is disabled. No new dependencies — drag-reorder via dnd-kit is a future polish.

Resource def knobs

Three new optional props on file/files FieldDefinition:

  • dropzone: "default" | "compact" | "minimal" | "avatar" | "inline"
  • progress: "bar" | "circular" | "pulse"
  • reorderable: boolean (default true; multi-file only)

These are pure overrides — the CLI doesn't emit them automatically; hand-edit the resource def to customise.

Migration

Existing scaffolded projects need three files refreshed: components/ui/dropzone.tsx, components/forms/fields/file-field.tsx, and components/forms/fields/files-field.tsx. Plus add FileSpreadsheet and Music to the export block in lib/icons.ts. Re-run grit generate resource for any resource to pull the updates — the files live once, not per resource.

v3.31.30June 24, 2026

File fields — first-class file + files types in grit generate resource. Replaces the awkward old pattern of treating uploads as string URLs.

New CLI syntax

Single file: grit generate resource Product --fields "image:file:image" scaffolds a single-image field that accepts jpg / png / gif / webp / avif / svg.

Multiple files: gallery:files:image for a multi-image gallery.

Bracketed accept-list for mixed types: attachment:file:[pdf,doc,image,video,zip]. Bare commas don't work because the top-level field separator is also ,; the parser is bracket-aware so the inner list stays glued together.

Accept aliases: image, video, audio, pdf, doc, excel, csv, zip, archive, all.

What gets generated

  • Go model: field typed as *files.FileRef (single) or files.FileRefs (multi), stored as JSON via GORM Value/Scan adapters in the new internal/files package.
  • Zod schema: imports FileRefSchema from the shared package — a single source of truth for the JSON shape.
  • Admin resource def: auto-emits accepts and maxSizeMB so the form's upload endpoint enforces the per-field validation.
  • FormBuilder: dispatches file / files types to the FileRef-aware FileField / FilesField components.
  • DataTable: file columns render as thumbnails for images, MIME-typed icons for everything else. Multi-file columns stack the first three thumbnails with a +N overflow chip.

API changes

POST /api/uploads now accepts ?accepts=<aliases>&max_size=<bytes> query params so the server validates against the per-field accept set (not just a global allowlist). Response shape changed to return a FileRef directly under data — drop-in for form state.

Defaults

  • Single file max: 5MB (300MB for video).
  • Multi-file count: 5.
  • Dropzone variant: the existing default boxed-dashed style. v3.31.31 adds 4 more variants (minimal, card, avatar, inline) + 3 progress variants + dnd-kit reorder.

Migration

Existing scaffolded projects need three things to pick up file fields: apps/api/internal/files/ (new package), the updated handlers/upload.go, and the refactored components/forms/fields/file-field.tsx + files-field.tsx + the new lib/file-accepts.ts. Re-run grit generate resource for any resource to get the updated templates — the new code lives once per project, not per resource.

v3.31.29June 24, 2026

Stats cards now refetch after create / update / delete. Total / This Week / This Month no longer go stale until manual reload.

The bug

Resource mutations all called invalidateQueries({ queryKey: [endpoint] }) on success, expecting React Query's prefix-matching to invalidate every query under that resource. But the stat-card query in PageHeader was keyed with ["stat", endpoint, field] — starting with the literal string "stat", not the endpoint. The invalidation never matched it, and a staleTime: 30_000 meant the value didn't even auto-refetch for 30 seconds.

Stats also use endpoints with query-string suffixes (e.g. /api/products?page_size=1&created_since=7d), so even if the key had started with the endpoint string, it wouldn't have matched the bare /api/products the mutation invalidates.

The fix

Stat queryKey now starts with the base endpoint (no query string): [endpoint.split("?")[0], "stat", endpoint, field]. Mutation invalidation prefix-matches it, and the staleTime is gone so the cards refetch immediately on success.

Migration

Existing projects: copy the new StatCardItem hook body from components/layout/page-header.tsx. One-function change.

v3.31.28June 24, 2026

Colored toasters. Success toasts are now green, errors red, warnings amber, info blue — instead of the previous neutral grey-on-grey that made every toast look identical.

What changed

The scaffolded <Toaster> in components/shared/providers.tsx now passes richColors, and app/globals.css bridges Grit's theme tokens (--success, --danger, --warning, --info) into sonner's palette slots via color-mix(). Each theme (atlas / aurora / pulse / midnight) already redefines those four tokens, so toasters automatically pick up the active brand colors — no per-theme overrides needed.

Migration

Existing projects: copy the new Toaster mount from providers.tsx and the [data-sonner-toaster] CSS block from globals.css (right after the scrollbar rules). All toast call sites in the scaffold already use toast.success() / toast.error() etc., so they pick up the new colors with zero code changes.

v3.31.27June 24, 2026

Fix React Rules of Hooks violation when formView: 'page' resources switch between list and form views. Reported by a learner who scaffolded a Category resource with formView: 'page' and clicked "New Category".

The bug

ResourcePage declared a few hooks at the top (useRouter, useSearchParams), then performed early returns for the form-page case (action=create or action=edit), then declared ~20 more hooks below (useState, useResource, useMemo, useCallback x many). When the URL changed and the component switched between list mode and form mode, the hook count changed between renders — React 19 throws "Rendered fewer hooks than expected."

The fix

Split ResourcePage into a thin router shell + a separate ResourceListView component. The router only calls useSearchParams and the routing helpers, then either renders one of the form variants or delegates to ResourceListView. The list view owns all 20+ list-mode hooks. Each function now has a stable hook count across renders, and the form path never mounts the list-mode hooks (so it doesn't spawn an unnecessary useResource fetch either).

Migration

Existing projects using formView: 'page' or 'page-steps' need to update apps/admin/components/resource/resource-page.tsx. Re-run grit generate resource <Name> on any resource (the file lives once, not per-resource) or copy the new structure from the scaffold output.

v3.31.14June 21, 2026

CLI prompt cleanup, Sentinel/Pulse links go to the API, and the Security + Performance dashboards finally show real data.

CLI: one form instead of three selects

grit new's architecture / frontend / theme prompts were running as three back-to-back huh.NewSelect calls. On Git Bash (MINGW64) the lack of full ANSI cursor-up support meant each re-render stacked into scrollback, producing the "same prompt printed twice" effect. Combined into a single huh.NewForm with conditional WithHideFunc groups — one tidy block of output, atomic submit.

Sentinel / Pulse links point at the API origin

/system/security "Open Sentinel" used a Next.js <Link href="/sentinel/ui">, which resolves relative to the admin host (:3001). Both Sentinel and Pulse are mounted on the Go API (:8080), so the links 404'd. Replaced with a plain <a> using NEXT_PUBLIC_API_URL.

Security + Performance dashboards return real data

Both pages were calling endpoints that either didn't exist (/api/admin/performance/summary) or returned a wrapped {data: {...}} envelope with raw Sentinel/Pulse internals under unfamiliar keys ({summary, score, threats, ...}). The React queries unwrapped axios' .data and looked for data.banned_ips_now / data.latency.p50 which didn't exist in the response — every KPI rendered as 0 or em-dash.

  • handlers/observability.go rewritten: hits Pulse's /overview, /runtime/current, /database/n1/ranked, and /errors endpoints, then reshapes the responses into a flat {latency, traffic, errors, saturation, slowest_routes, n1_detections, recent_errors} envelope. No more {data: ...} wrapper.
  • handlers/security.go rewritten: hits Sentinel's /ip/blocked, /analytics/summary?window=24h, and /threats (the prior /dashboard/summary endpoint doesn't exist in this Sentinel version); returns the flat {banned_ips_now, auto_bans_24h, active_bans, recent_threats, ...} shape the page expects.
  • Performance page corrected to hit /api/admin/observability/summary instead of the nonexistent /performance/summary.
v3.31.26June 21, 2026

Fix two bugs in the FormShare dispatcher template reported by a learner who created a fresh project and ran grit generate resource Category + Product. The API failed to build with syntax error: non-declaration statement outside function body.

Bug 1 — marker collision with doc comment

The scaffolded services/form_share_dispatch.go doc comment literally contained the string // grit:form-share:dispatch marker. When the generator ran injectBefore for a new resource, it found that occurrence first (above the function, outside any function body) and inserted every case there. The function's switch stayed empty, and the cases sat in package scope where they produced a syntax error.

Fix: rephrased the doc comment to describe the marker without containing the marker string.

Bug 2 — function param named "body", inject uses "fields"

The dispatcher's third parameter was body map[string]interface{}, but every injected case uses json.Marshal(fields). Even if Bug 1 hadn't hit first, the cases would have failed to compile with undefined: fields.

Fix: renamed the parameter to fields so it matches what the inject template produces.

Migration

Existing projects that ran grit generate resource X on or after v3.31.20 may have a broken form_share_dispatch.go. To fix:

  1. Open apps/api/internal/services/form_share_dispatch.go.
  2. Move any case "X": blocks that landed above the function back inside the switch below.
  3. Rename the function parameter from body to fields if needed.
  4. Rephrase the doc comment so it doesn't contain the literal marker string.
v3.31.25June 21, 2026

Audit trail for public form submissions. The last deferred item from PLAN_FORMS_AND_SHARING.md. Operators can now see every submission that came in through each share — with timestamp, IP, and User-Agent.

What landed

  • New FormSubmission model — one row per successful public submission. Captures share_id, resource_name, record_id, IP, User-Agent, timestamp. Soft-deletable for retention.
  • PublicSubmit writes the audit row after a successful dispatch. Best-effort: failure to write the audit row does NOT roll back the user's submission. They still get their record; the admin just misses one line in the trail.
  • New admin endpoint: GET /api/admin/form-submissions?share_id=&resource_name= — paginated audit log, filterable by share or resource.
  • Admin UI: the /system/form-shares page gains an Audit button per share. Click → modal listing the 100 most recent submissions for that share with timestamp, record ID, IP, and a truncated UA tooltip.

Why a separate table, not a column

An earlier draft considered adding source_share_id as a column on every scaffolded model. That approach is invasive — every existing project would need a migration to add the column to Contact / Application / Lead / etc. The audit-table approach is purely additive: new project or existing, grit migrate creates the new form_submissions table and existing models stay untouched.

Bonus: the audit table captures richer data than a column could (IP + User-Agent), which is useful for spam triage and compliance.

Phase recap, complete

Every numbered item on PLAN_FORMS_AND_SHARING.md has shipped:

  • v3.31.16 — sync auto-add admin fields
  • v3.31.17 — formView sheet / modal / page
  • v3.31.18 — form groups + per-group PATCH
  • v3.31.19 — column-pack auto-detection
  • v3.31.20 — public form sharing
  • v3.31.21 — grit expose form / table
  • v3.31.22 — grit add web-auth
  • v3.31.23 — course lessons + tests
  • v3.31.24 — --public-share + --token flags
  • v3.31.25 — audit trail (this release)
v3.31.24June 21, 2026

grit expose form gains --public-share + --token flags — the deferred public-form variant from the v3.31.21 changelog now ships. Scaffold a public-facing form at any URL of your choosing that posts to a FormShare endpoint instead of the authenticated hook.

Usage

# Hard-code the token into the page
grit expose form Contact \
  --to apps/web/app/contact-us/page.tsx \
  --public-share \
  --token 9CkLh7gJZQrPeNwMo3F8x_iVjA8U2nXt

# Or omit --token and let the page read NEXT_PUBLIC_FORM_TOKEN at runtime
grit expose form Contact \
  --to apps/web/app/contact-us/page.tsx \
  --public-share

What the generated page does

  • Posts to /api/public/forms/<token>/submit — no auth required, no useCreate hook imported.
  • Probes /api/public/forms/<token> on mount to confirm the share is enabled and to learn whether to render a password gate.
  • Shows an amber "Form unavailable" card when the token is missing, disabled, or invalid — instead of a blank form.
  • Token resolution: literal from --token when set; otherwise process.env.NEXT_PUBLIC_FORM_TOKEN at module load. Pick whichever fits your env model.

When to use this

Use --public-share when you want a branded public form at your own URL (/contact-us, /apply, /leads) instead of the default /forms/[token] page. The dispatcher, rate limits, and password gate behave identically; only the URL and styling are yours to control.

Lesson update

The grit-expose lesson now has a "--public-share: a public form on YOUR url" section with both embed-token and env-token examples.

v3.31.23June 21, 2026

Docs + tests follow-up to the PLAN_FORMS_AND_SHARING.md arc. Phases 2-4 shipped without dedicated course lessons; this release closes that gap.

Three new course lessons

Chapter 4 ("Code Generation & Type Sync") picks up a new module — Going public — with three lessons covering the post-resource lifecycle:

  • grit expose form / table — when to use each, anatomy of the commands, field filtering, combining with form sharing.
  • Public form sharing — the dispatch pattern, password gating, sharing the link, disabling and regenerating, what it can't do (yet).
  • Protecting web pages — middleware vs ProtectedWebRoute, when each one fits, how they layer.

Unit tests for the expose package

internal/expose now has 10 unit tests covering the security-critical field filter (autoFields drops framework columns, pointer + value associations, slice associations; keeps all 7 primitive types), label generation (acronym handling), and the pluralisation helpers (pluralPascal, pluralKebab). Plus path validation for resolveTarget. All passing.

Course chapter 4 now has 13 lessons

Up from 10 in the previous release. The chapter covers the full resource lifecycle from initial generation through customisation, sharing, exposure, and protection — end to end.

v3.31.22June 21, 2026

grit add web-auth — Phase 4 of PLAN_FORMS_AND_SHARING.md, the final phase. With this release, every phase of the forms / sharing initiative has shipped (v3.31.16 → v3.31.22).

The web app already shipped with login / register / forgot-password pages and a useMe() hook. What was missing: a way to mark which customer-facing pages require sign-in. grit add web-auth closes that gap with two complementary patterns.

Files scaffolded

  • apps/web/middleware.ts — SSR cookie redirect. Runs on every Next.js request, checks for the grit_access HttpOnly cookie, redirects to /login?next=… when missing on a protected path. Also bounces already-signed-in visitors off the login/register pages so they don't see a form they don't need. Edit the PROTECTED_PATHS and AUTH_PATHS arrays to customise.
  • apps/web/components/ProtectedWebRoute.tsx — client-side wrapper. Wraps a page with <ProtectedWebRoute>{children}</ProtectedWebRoute> to enforce auth in cases where middleware can't help — e.g. role-gated content (the cookie doesn't carry the role; useMe() returns the full user). Supports an optional roles prop.

The two patterns

  • Middleware (SSR) — fast, no network round-trip per request, no flash of unauthorized content. Use for "is the visitor signed in?" pages: account dashboards, checkout, member-only content.
  • ProtectedWebRoute (client) — makes a real /api/auth/me probe. Catches expired-but-present cookies and supports role checks. Use it when middleware isn't enough.

Behavior

Both files are idempotent — re-running grit add web-auth without --force skips existing files. The scaffold prints a clear notice so operators know what was created and what was preserved.

Phase recap (v3.31.16 → v3.31.22)

  • v3.31.16 — sync auto-adds new model fields to admin resource files
  • v3.31.17 — formView sheet / modal / page
  • v3.31.18 — form groups + per-group PATCH save
  • v3.31.19 — column-pack auto-detection (name + email)
  • v3.31.20 — public form sharing (token + bcrypt password)
  • v3.31.21 — grit expose form / grit expose table
  • v3.31.22 — grit add web-auth (this release)
v3.31.21June 21, 2026

grit expose form / grit expose table — Phase 3 of PLAN_FORMS_AND_SHARING.md.

Two new CLI commands that scaffold a Next.js page in apps/web/ for an existing resource. The page consumes the auto-generated React Query hook directly, so you get list/create flows on a customer-facing site without re-implementing anything.

Commands

grit expose form Contact --to apps/web/app/contact-us/page.tsx
grit expose table Contact --to apps/web/app/contacts/page.tsx
  • Each command parses apps/api/internal/models/<snake>.go to determine the resource's primitive fields. Relationship pointers (Group *Group or Group Group) and slices (Tags []Tag) are filtered out — only fields that can render as one <input> or one table cell make it through.
  • Both commands refuse to overwrite an existing file unless you pass --force — protects hand-customised pages from accidental loss.
  • Generated pages are plain Tailwind (no admin chrome), suitable for embedding on a marketing site or a customer dashboard.

Supporting fixes

  • Web hook imports: the generator now branches its apiClient import path by app — @/lib/api-client for admin, @/lib/api for web. Resolves a pre-existing "Cannot find module" error in web-side resource hooks.
  • Scaffolded apps/web/lib/api.ts now re-exports apiClient = api so generated hooks resolve symmetrically across both apps.
  • Web package.json gains @hookform/resolvers as a dep (was only in admin before).
  • ParseGoStructs exported from the internal/generate package for reuse by the new internal/expose package.

Known limitations

  • Generated forms don't use the shared Zod schema for validation — the schema's camelCase field names don't match the API's snake_case JSON keys. Forms submit snake-case keys directly; server-side validation is the source of truth. Add client-side validation by hand if you need it.
  • Forms have one field per primitive column. Custom widgets (rich text, image uploaders, relationship dropdowns) need manual additions after generation.
  • --public-share / --public flags (post via the public form-share endpoint instead of via the auth'd hook) are still on the roadmap.
v3.31.20June 21, 2026

Phase 2 of PLAN_FORMS_AND_SHARING.md — public form sharing. Generate a token-protected link for any of your resources and anyone with the link can submit the form, no admin login required. Optional bcrypt password on the share for an extra gate.

What landed (end-to-end)

  • FormShare model (token, optional bcrypt PasswordHash, enabled, submission count, label). Auto-migrated on grit migrate.
  • Admin handler + routes: GET/POST/PATCH/DELETE /api/admin/form-shares.
  • Public handler + routes (no auth, no CSRF): GET /api/public/forms/:token + POST /api/public/forms/:token/submit. Both paths are listed in Sentinel's ExcludeRoutes so the WAF doesn't block public JSON bodies.
  • Marker-driven resource dispatch: every grit generate resource appends a case to services/form_share_dispatch.go that JSON-decodes the public payload into the resource's model + calls db.Create(). Whitelisted by name — unknown resources can't be submitted publicly.
  • Admin page at /system/form-shares: list shares, create new (with password), toggle enabled, copy public URL, delete.
  • Public web page at apps/web/app/forms/[token]/page.tsx — a minimal name/email/phone/message form that posts to the public submit endpoint. Tailored forms for other resource shapes come via grit expose form in Phase 3.

Threat model

  • Resource whitelisting: the dispatch service's switch statement is the gate. A share token can't conjure a record for a resource that hasn't been explicitly added.
  • Field whitelisting: each resource case JSON-decodes onto its typed model. Unknown JSON keys are silently ignored; private fields (id, created_at, …) are untouched.
  • Rate limiting: Sentinel still rate-limits the public path by IP — the WAF body inspection is the only thing skipped.
  • Password (optional): bcrypt cost 10. Submitted as _password alongside the fields; rejected with 401 if mismatched.

Deferred to v3.31.21

  • Audit trail: a source_share_id column on each submitted record so admins can filter "show me public submissions" per resource.
  • Per-resource public form pages: Phase 3's grit expose form <Resource> will scaffold a tailored public page with the exact field shape, replacing the generic name+email+phone+message default.
v3.31.19June 21, 2026

Column-pack auto-detection. Phase 1.4 of PLAN_FORMS_AND_SHARING.md.

Generate a resource with both name and email (or both first_name and last_name) and the table now ships with those fields packed into a single stacked column — name on top, email muted below. No hand-written cell: callback needed.

How it works

  • New helper: apps/admin/components/tables/stacked-cell.tsx. Exports a StackedCell({ top, bottom }) function returning two-line JSX. Called as a function (not JSX syntax) so resource files stay .ts.
  • Generator now runs a pack-detector over the resource's field list. When a pattern matches, the absorbed fields are silently skipped and the packed line is emitted in their primary's slot.
  • Import of StackedCell is conditional — resources without a pack stay clean.

Patterns recognised today

  • name + email → "Contact" column
  • first_name + last_name → "Name" column

Both are easy to extend in internal/generate/column_packs.go. Money + currency badge, status + relative date, and a few others are roadmap.

Existing resources

Pre-v3.31.19 resources don't auto-pack. Either add the pack by hand (the customising-tables lesson has the recipe), or wait for grit pack table <Resource> in a future release.

v3.31.18June 21, 2026

Form groups + per-group PATCH save. Phase 1.3 (partial) of PLAN_FORMS_AND_SHARING.md.

Long Update views with 10+ fields used to save the whole record on every click — risky when two operators edit different sections at once, slow because the payload is large, and tedious because every field had to be re-validated. Define form.groups and each group renders as its own Card on the Update page, with its own Save button that PATCHes only that group's fields.

What landed

  • New Patch handler on every generated resource. Whitelists writable columns so the partial endpoint can't be tricked into setting id / created_at /deleted_at / version from the client.
  • PATCH /api/<plural>/:id route registered alongside PUT for every resource (both standard and role-restricted route groups).
  • usePatchResource(endpoint) hook in the admin's use-resource module. Same shape as useUpdateResource but calls PATCH and toasts "Saved" on success.
  • GroupDefinition type on FormDefinition.groups. Each group is { title, description?, fields: string[], scope?: "create" | "update" | "both" }.
  • <UpdateGroups> component renders each scope: "update" or "both" group as a separate Card on the Update page (when formView: "page" + form.groups are defined).
  • ResourcePage dispatcher: when editing + groups present, route to UpdateGroups; otherwise fall back to the single-form FormPage.

The "create-and-update" pattern

Use scope: "create" on the minimal required fields and scope: "update" on the rest. Operators get a frictionless Create form; detailed editing happens on the Update page as cards with partial saves.

Deferred to v3.31.19

Group rendering on the Create flow as a multi-step wizard. The existing steps field still works for that. v3.31.19 unifies them so groups drive both contexts.

v3.31.17June 21, 2026

Form render modes — sheet / modal / page. Phase 1.2 of PLAN_FORMS_AND_SHARING.md.

The formView field on defineResource now accepts "sheet" as an explicit value, and "modal" renders as a proper centered dialog instead of a sheet. Defaults are unchanged — resources without an explicit formView still get the long-form-friendly drawer.

The three rendering choices

  • "sheet" (default) — right drawer on desktop, bottom sheet on mobile. Best for long forms and multi-line fields.
  • "modal" — centered dialog over a backdrop. Best for short focused forms (1–6 fields).
  • "page" — dedicated route via ?action=create|edit. Best for very long forms or anything that needs shareable URLs.

What shipped

  • New FormSheet component (apps/admin/components/forms/form-sheet.tsx) — the long-form-friendly drawer, formerly the implementation of FormModal.
  • FormModal rewritten as a proper centered dialog (max-w-md, backdrop blur, padding).
  • ResourcePage dispatcher picks the right component based on resource.formView.
  • ResourceDefinition type union expanded to include "sheet".

Migrating

If you previously set formView: "modal" explicitly and want the old sheet behavior, change it to "sheet". Resources that left formView undefined stay on the sheet — no migration needed.

v3.31.16June 21, 2026

grit sync now auto-adds new model fields to admin resource files. Phase 1.1 of the PLAN_FORMS_AND_SHARING.md roadmap.

Add a column to a Go model, run grit migrate + grit sync, and the field now appears in apps/admin/resources/<plural>.ts as both a column and a form input — with a sensible default type inferred from the Go type. Customised entries (labels, helper text, badges, custom cell renderers) are never touched.

How it works

The generator now emits marker comments around the auto-managed columns + form fields:

columns: [
  // grit:cols:auto-start
  { key: "name", ... },
  // grit:cols:auto-end
],
form: {
  fields: [
    // grit:fields:auto-start
    { key: "name", ... },
    // grit:fields:auto-end
  ],
},

Sync diffs Go model fields against the file. For each field with a key: not found anywhere in the file, it inserts a default entry above the auto-end marker. Sync is insert-only — it never modifies or removes existing entries.

Backward compatibility

Resources scaffolded before v3.31.16 don't carry the marker comments. Sync prints a per-resource warning and skips them. To enable auto-add on an existing resource, hand-edit the file to wrap its columns array and form fields array with the four marker lines once. After that, future syncs pick the file up.

What's next (Phase 1 continued)

v3.31.17+ ships the rest of Phase 1 per PLAN_FORMS_AND_SHARING.md:

  • Form render mode (sheet | modal | page)
  • Form groups (steps in create, cards in update) + PATCH endpoint for per-group saves
  • Column-pack default heuristic + grit pack table <Resource>
v3.31.15June 21, 2026

Auth UX overhaul + admin polish from a real app-building session. Seven concrete fixes driven by feedback while building a contact-app on the prior release.

Framework fixes

  • Protected admin routes no longer flash a blank white page when the session expires or the API restarts. The admin layout now redirects to /login on both network errors AND null user (401), and shows a spinner while the redirect fires. (internal/scaffold/admin_layout_files.go)
  • Login page bounces to /dashboard when the session cookie is still valid — no more seeing the login form while you're already signed in.
  • New SessionWatchdog component surfaces a modal at 14:30 of idle time with a 30s countdown — "Stay signed in" refreshes via /api/auth/refresh, "Sign out" or timeout calls useLogout(). Configurable via NEXT_PUBLIC_SESSION_IDLE_MS and NEXT_PUBLIC_SESSION_COUNTDOWN_MS.
  • Sentinel WAF no longer blocks richtext admin POSTs. The 64 KB body cap is now 1 MB (richtext + embedded inline images need it), and admin write endpoints with HTML payloads (/api/blogs, /api/posts, /api/articles, /api/uploads) are listed under ExcludeRoutes so the WAF's XSS detection stops flagging every <p> tag.
  • Generated resource tables drop the ID column by default. UUIDs are noisy and rarely scanned by eye — operators who need it can add it back manually.
  • ColumnDefinition gains an optional cell?: (row) => ReactNode renderer — pack multiple fields into one column (name + email stacked, price + currency badge, status pill + relative date) without dropping out to a hand-written page.tsx. Takes precedence over format and badge when set.
  • grit sync prints a heads-up that it does NOT update the admin resource definition, pointing operators at apps/admin/resources/<plural>.ts when a new model field doesn't show up in the admin form.

Chapter 4 — 4 new lessons

Chapter 4 now has 11 lessons (up from 7) — fully covering the post-generation flow:

  • grit remove resource — the rollback half of the lifecycle.
  • Customising admin forms — all 17 field types, helper text, multi-step flows.
  • Customising admin tables — formats, badges, filters, and the new cell() render function with three column-packing recipes.
  • Using the generated API from the web app — list, search, detail, create form with the auto-generated React Query hook and shared Zod schemas.
v3.31.13June 21, 2026

Root .env is now the single source of truth for THEME + SOCIAL_AUTH_ENABLED. Setting SOCIAL_AUTH_ENABLED=false in the monorepo's root .env didn't hide the Google / GitHub buttons even after a server restart — Next.js only auto-loads .env from its own package directory (apps/admin/, apps/web/), so process.env.SOCIAL_AUTH_ENABLED inside next.config.ts was undefined and the || "true" fallback always won.

Both scaffolded next.config.ts files now read the root .env directly via a tiny inline parser before the env block is evaluated. Shell env still wins (only unset keys are filled in), so CI / Docker overrides are unaffected. After upgrading, restart pnpm dev (Next.js reads env at boot, not on file-watch).

v3.31.12June 21, 2026

System Health / Security / Performance pages build clean. The scaffolded /system/health, /system/security, and /system/performance pages import five lucide icons (CheckCircle, Server, HardDrive, Clock, Gauge) that weren't re-exported from apps/admin/lib/icons.ts. A fresh pnpm --filter ./apps/admin build failed with Export <Name> doesn't exist in target module. Added the five names to both the lucide-react import block and the named re-export block. All 24 admin routes now prerender on a fresh scaffold.

v3.31.11June 21, 2026

air entrypoint points at the built binary, not the source dir. v3.31.10 fixed the .exe extension but the scaffolded .air.toml still set entrypoint = "./cmd/server" — and air tries to exec the entrypoint as the binary, so Windows hit CMD will not recognize non .exe file. Per air's docs, entrypoint names the built binary (the same role build.bin plays). Fixed to entrypoint = "./tmp/server.exe". Verified with a live grit start server in a fresh scaffold plus a /api/health curl returning the full database/redis/jobs/email shape.

v3.31.10June 21, 2026

Scaffolded .air.toml uses an .exe binary on Windows. v3.31.9 shipped grit start with air-backed hot reload, but the generated .air.toml used bin = "./tmp/server". Windows refuses to CreateProcess an extension-less file, so starting the dev loop popped a "Select an app to open 'server'" dialog instead of running the API. Switched to cmd = "go build -o ./tmp/server.exe ./cmd/server" in the scaffolded template so Windows can execute the output directly.

v3.27.0June 20, 2026

Admin auth sweep + fresh-scaffold type-clean. Closes the last gap left by v3.26.0's HttpOnly cookie story: the admin app now uses cookies end-to-end too, js-cookie is gone from both frontends, OAuth no longer leaks tokens via URL params, and both apps/web + apps/admin return zero type errors on a fresh scaffold for the first time.

Admin uses HttpOnly cookies

  • apps/admin/lib/api-client.ts drops js-cookie, adds withCredentials: true, and echoes the grit_csrf cookie into X-CSRF-Token on every mutation.
  • apps/admin/hooks/use-auth.ts imports User, LoginRequest, RegisterRequest, AuthResponse, ApiResponse from @repo/shared/types instead of declaring them inline. useMe returns null on 401 instead of throwing. useLogout doesn't clear tokens locally — the API does it via Set-Cookie.
  • The admin root redirect page (app/page.tsx) drops the Cookies.get('access_token') check (which couldn't see HttpOnly cookies anyway) in favour of a useMe() probe.
  • The 401-refresh interceptor now POSTs /api/auth/refresh with an empty body — the API reads grit_refresh from the cookie and issues a new grit_access via Set-Cookie.
  • profile delete drops Cookies.remove calls — the Go DeleteProfile handler now calls ClearAuthCookies as part of its response.
  • js-cookie and @types/js-cookie dropped from apps/admin/package.json.

OAuth without URL leakage

The Go OAuth callback handler now calls SetAuthCookies BEFORE the 307 redirect to /auth/callback. The cookies travel on the redirect response itself, so the callback page no longer needs to read access_token and refresh_token from the URL. Tokens never appear in browser history, server access logs, or Referer headers. Closes the gap left when v3.26.5 fixed email/password.

UUID vs number ID drift cleaned up

  • useBulkDeleteResource signature ids: number[]ids: string[] (Grit's models all use UUID primary keys).
  • form-modal.tsx + form-page.tsx + their -steps variants stop casting item.id / editId to Number — they pass the IDs through as strings, matching useUpdateResource / useResourceItem.
  • relationship-select-field.tsx + multi-relationship-select-field.tsx use String(item.id) instead of asserting as number.
  • hooks/use-system.ts dropped its inline Upload interface and imports from @repo/shared/types; UploadListResponse is now an alias for PaginatedResponse<Upload>.
  • Admin icon map: Cpu, Zap, Globe added; Shield was imported but not re-exported — fixed. System observability / security pages corrected from @/lib/api to @/lib/api-client.

Net effect: a fresh grit new pnpm install pnpm exec tsc --noEmit on apps/web AND apps/admin returns zero type errors for the first time. The Go API builds and template tests pass unchanged.

v3.26.5June 20, 2026

Web hooks finally consume packages/shared + use the v3.26.0 HttpOnly cookie auth. Closes a contradiction a learner spotted: the docs teach "shared types live in packages/shared" but the scaffolded use-blogs and use-auth hooks duplicated User and Blog inline.

Type imports

  • use-blogs.ts now imports Blog + PaginatedResponse from @repo/shared/types.
  • use-auth.ts imports User, LoginRequest, RegisterRequest, AuthResponse, ApiResponse from the same barrel.
  • lib/auth-provider.tsx imports User from @repo/shared/types instead of a 10-line local copy.
  • Web app now has @repo/shared: workspace:* in its package.json (was missing). next.config.ts gets transpilePackages: ['@repo/shared'] so SWC picks up the TS source.

Auth flow uses HttpOnly cookies end-to-end

  • Axios client gets withCredentials: true so the browser actually attaches the grit_access / grit_refresh cookies the API issues.
  • A request interceptor echoes the grit_csrf cookie into X-CSRF-Token on every state-changing request — required by the AutoCSRF middleware that v3.26.0 wired in.
  • use-auth.ts dropped js-cookie, storeTokens / clearTokens / getAccessToken, and every Authorization: Bearer header attachment. useMe returns null on 401 instead of throwing.
  • Login + register pages no longer call Cookies.set('access_token'). The API sets HttpOnly cookies via Set-Cookie; the browser stores them; JS never touches tokens.

Known gaps deferred to a follow-up release

  • OAuth callback page still reads tokens from URL params and sets them via Cookies.set. Proper fix requires the Go-side OAuth handler to set cookies before redirecting.
  • Admin app still uses js-cookie + Bearer header auth throughout. Bigger refactor (TOTP, OAuth begin/callback, profile page, multi-step token refresh) that warrants its own release.
v3.26.4June 15, 2026

grit start now actually starts both, like the help said it would.

The bug

In a web project, grit start (no subcommand) was falling through to cmd.Help() — printing the available subcommands and exiting. The command's ownLong description said it would start both the API and the client, but only grit start server and grit start client actually did anything.

What changed

  • grit start in a web project now spawns go run cmd/server/main.go (in apps/api/) and pnpm dev (at the project root) in parallel.
  • Output from both processes is streamed to the same terminal with a coloured [api] / [web] prefix per line so a developer can tell whose log is whose without splitting panes.
  • Ctrl+C (and SIGTERM) is forwarded to both children. If either child exits on its own, the other is shut down too — no zombie processes left behind.
  • Desktop projects are unchanged — grit start still calls wails dev. The subcommands grit start server and grit start client still work if you only want one side.
v3.26.3June 15, 2026

Redis + MinIO host ports moved to dodge native-install collisions. Same pattern as v3.26.2 did for Postgres, applied to the two other ports learners actually clash on.

What changed

  • Redis: dev host port 6379 → 6380. Native installs (Memurai on Windows, brew install redis, apt install redis-server, WSL Redis) all bind 6379.
  • MinIO S3 API: dev host port 9000 → 9002. Portainer's admin UI defaults to 9000; SonarQube and a handful of monitoring stacks grab it too.
  • MinIO console: dev host port 9001 → 9003. Less common collision but kept in sync with the API port shift.
  • Mailhog kept on 1025 / 8025. Almost zero dev machines have anything on those — shifting them just adds learner confusion without preventing real failures.

Inside the Docker network

Containers still listen on the canonical ports — Redis on 6379, MinIO on 9000 + 9001. The host-port shifts only affect how you reach them from your laptop. Prod compose is unchanged because inter-container traffic uses the docker network hostnames (redis, minio) and container ports.

Where else this surfaces

  • .env: REDIS_URL=redis://localhost:6380 and MINIO_ENDPOINT=http://localhost:9002.
  • The CLI "next steps" banner after grit new now prints the new host ports.
  • Scaffolded README's services table and the docs lessons (Docker primer, dev-servers, batteries/redis-cache, batteries/s3-storage) updated to match.
v3.26.2June 15, 2026

Postgres host port 5432 → 5434 to dodge Windows WinNAT reservations. Closes the "An attempt was made to access a socket in a way forbidden by its access permissions" bind error that hit Windows users on a fresh docker compose up -d.

What was wrong

Even with no process visibly holding port 5432, Docker Desktop on Windows would refuse to bind it. Cause: the WinNAT service / Hyper-V Virtual Switch silently reserves TCP port ranges at boot, and 5432 sits inside one of the common reservations on Docker Desktop + WSL2 default installs.

What changed

  • Dev docker-compose.yml now publishes Postgres on host port 5434 (not 5432). Container port stays 5432 inside the Docker network.
  • .env sets POSTGRES_PORT=5434 so the Go API connects to the same host port.
  • docker-compose.prod.yml pins POSTGRES_PORT=5432 in the api service environment because inter-container traffic uses the container port, not the dev host port.
  • Docker primer lesson gains error 2a, covering the Windows-specific bind error with the netsh int ipv4 show excludedportrange diagnostic and three fix paths.

Net effect: a fresh grit new docker compose up -d grit migrate now succeeds on Windows machines whose Hyper-V reservation overlaps 5432 — without any user-side intervention.

v3.26.1June 15, 2026

Single source of truth for Postgres credentials — fresh scaffolds Just Work. Closes the "SQLSTATE 28P01: password authentication failed" trap that bit every learner whose .env and docker-compose.yml drifted apart.

The bug

v3.25.x and v3.26.0 scaffolds wrote three disagreeing copies of the DB credentials: docker-compose.yml hardcoded grit:grit, .env's DATABASE_URL used grit:grit, and .env's POSTGRES_PASSWORD said change-me-in-production. The moment a learner edited one, the others were out of sync and grit migrate failed.

The fix

  • One canonical POSTGRES_* block in .envPOSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB / POSTGRES_HOST / POSTGRES_PORT.
  • POSTGRES_PASSWORD is now generated at scaffold time as a 48-hex-char random string (alongside JWT_SECRET, PULSE_PASSWORD, etc.) — even APP_ENV=production is safe on first boot.
  • docker-compose.yml reads from .env via $${VAR:-grit} substitution. No hardcoded credentials anywhere.
  • The Go API builds DATABASE_URL from the same POSTGRES_* parts at startup. Set DATABASE_URL only if you want to point at external Postgres (Neon, Supabase, RDS) or SQLite — it's the explicit escape hatch.
  • Prod compose now sets POSTGRES_HOST=postgres in the api environment so the Go binary finds the postgres container on the docker network. No embedded DATABASE_URL in the compose YAML anymore.

A fresh grit newdocker compose up -d grit migrate now succeeds without any editing.

v3.26.0June 14, 2026

Concepts ch.2 expansion, Docker hardening, and the long-overdue grit.json fix. A teaching + security release driven by real student feedback.

Concepts course chapter 2

  • "Tour of your project" lesson rewritten end-to-end. The original 30-second map was missing most of what the scaffold actually produces. The new lesson walks every folder + file matched against a real fresh scaffold: every package under apps/api/internal (25+ packages in a reference table), the full apps/web + apps/admin layouts, packages/shared + packages/grit-ui, tests/k6 (6 scripts), e2e/ (Playwright), .claude/, .github/, and every root config file.
  • New "A Docker primer" lesson inserted between project-tour and dev-servers. Most learners stall at docker compose up -d because nobody taught Docker first. The new lesson covers what Docker is, image/container/volume, install per OS, how Grit uses Docker, the 12 commands you'll actually type, the 6 errors learners hit + their fixes, plus a full "Run Grit without Docker" path (Neon + Upstash + Cloudflare R2 + Resend).

Docker scaffold hardening

  • docker-compose.yml binds every port to 127.0.0.1, not the Docker default 0.0.0.0. Coffee-shop wifi can no longer reach Postgres with grit:grit credentials.
  • docker-compose.prod.yml documented as reverse-proxy-first. A top-of-file comment block spells out the security posture: nothing uses ports:, only expose:. Postgres + Redis have NO host binding at all in prod. Traffic must arrive via Traefik / Caddy / nginx / Dokploy on the same Docker network.

Scaffold fixes

  • grit.json now writes the real CLI version. Previously hardcoded to 3.3.0 (a leftover placeholder from when the grit.json schema was at 3.3). Fresh projects now show the actual scaffolding CLI version (3.26.0 today). Closes the "is my project version really 3.3?" confusion.

Docs site

  • Single-source-of-truth version constant in config/site.ts. The header badge, install lesson example output, verify-install lesson, animated terminal, and changelog all read from one place — no more drift between the CLI version and what the website shows.
v3.25.2May 31, 2026

Smarter grit update + docs sweep. Two follow-ups to the v3.25 install/update flow.

Update command

  • Short-circuits when already on latest. The version check that previously only ran on the GitHub-binary path is now lifted to the top of grit update — both the Go-install and GitHub-binary strategies skip their work when there's nothing to do. One HTTP round-trip, then exit. (Was: always rename + go install + cleanup, even when already current.)
  • Unix path no longer deletes before installing. POSIX keeps the running process's inode alive when the file at the same path is overwritten, so go install can write straight on top. We previously did os.Remove first, which left the user stranded if go install failed.
  • Windows rename now rolls back on failure. The .exe-locked-while-running dance (rename current to .old → write new → delete .old) now restores the original binary if go install fails before writing the new one, so a flaky network or proxy issue can't leave the user with no usable grit.

Docs sweep

  • Replaced every go install github.com/MUKE-coder/grit/... install reference across docs pages, tutorials, courses, and the structured-data FAQ schema with the v3.25 one-line install script (with go install kept as a secondary option for power users with Go installed).
  • Hero terminal animation now opens with curl -fsSL https://gritframework.dev/install.sh | sh instead of go install.
v3.25.1May 31, 2026

Scaffold now generates real secrets — and SQLite uncommenting is clean. Two papercuts that turned into roadblocks the moment anyone flipped APP_ENV=production on a fresh project.

Fixes

  • Random secrets at scaffold time. grit new now generates cryptographically random values (crypto/rand → hex) for JWT_SECRET, SENTINEL_PASSWORD, SENTINEL_SECRET_KEY, and PULSE_PASSWORD when writing .env. Previously these shipped as your-super-secret-... and admin/sentinel / admin/pulse, which Sentinel v2 and Pulse v1 explicitly refuse to start with in release mode. A fresh scaffold now boots cleanly in production mode with both dashboards mounted — no manual openssl rand step required.
  • Clean SQLite uncomment line. The commented-out SQLite DSN previously had multiple leading spaces (#   DATABASE_URL=sqlite:...), so removing the left a line with leading whitespace. godotenv tolerated it, but it was ugly and confusing. Now single-#-prefixed for a clean uncomment.
  • k6 tutorial: turn Sentinel + Pulse off for the bench. Both sit in the request middleware chain. Leaving them on while load-testing means measuring them, not Gin. Step 3 now sets SENTINEL_ENABLED=false + PULSE_ENABLED=false alongside the SQLite switch.
v3.25.0May 31, 2026

One command to update: grit update. The CLI now checks GitHub for the latest version, compares against the running binary, and — depending on whether Go is on your PATH — either runs go install ...@latest or downloads the matching binary from the GitHub release and swaps it in place. Atomic swap is handled by inconshreveable/go-update, so it works correctly even on Windows where you can't overwrite a running binary.

What changed

  • Smart grit update — first checks GitHub releases. If you're already on latest, exits in a single round-trip with Already on the latest version. No more wasted go install runs.
  • No-Go-toolchain mode. If the go binary isn't on PATH, grit update falls back to the GitHub-binary path automatically. Means grit can keep itself current even for users who installed from the prebuilt archive and never touched Go.
  • --from-release flag forces the GitHub-binary path even when Go is installed. Useful if you're behind a corporate proxy that blocks the module proxy but allows github.com.
  • Alias grit self-update for discoverability — same command, clearer intent than update.

How to get it

This is the bootstrap release — you need to install v3.25.0 manually one time, then future versions are a single command:

# from any directory, with Go on PATH:
go install github.com/MUKE-coder/grit/v3/cmd/grit@v3.25.0

# from then on:
grit update
v3.24.0May 31, 2026

SQLite, AI prompts everywhere, and a Learnings journal. Three shipping threads: (1) the scaffolded API now speaks SQLite, not just Postgres — flip DATABASE_URL=sqlite:./app.db in .env and you skip Docker entirely; (2) every tech-kit page now ships a copyable starter prompt for claude.ai plus a new four-step AI Integration wizard that generates a tailored planning prompt; (3) a new Learnings section opens an engineering journal — first entry walks a stateless service + k6 load test from grit new --api all the way to a committed p50/p95/p99 latency chart.

Scaffold & framework

  • SQLite support. internal/database/database.Connect now branches on DSN prefix: sqlite://path, sqlite:path, sqlite::memory:, or Postgres for anything else. Uses github.com/glebarez/sqlite (pure Go, no CGO) so it works on Windows without a C toolchain. Existing Postgres setups are unchanged.
  • .env documentation. Both .env and .env.example now show all three DSN shapes inline.
  • Demo DEMO_MODE bypass. Sentinel v2 + Pulse v1 refuse to start in release mode with default credentials. The public Grit demo now opts in via DEMO_MODE=true in demo/internal/routes/routes.go — production deploys still get the gate; the publicly pokeable demo skips it.

Docs site

  • Per-kit starter prompts. All 7 tech-kit pages (single, single-vite, double, triple, api, mobile, desktop) now have a "Plan this kit with an AI" section. One copy button gives you a prompt to paste into claude.ai with your idea — Claude returns project-description.md, project-phases.md, design-style-guide.md, and prompt.md, the four planning files you feed to Claude Code.
  • New AI Integration page. A four-step wizard (Platform → Tech Kit → Use case → Your prompt) that customizes the prompt per project shape. Modelled on the DGateway integration helper.
  • New Learnings section. /docs/learnings is an engineering journal. The first entry — a stateless service + k6 load test walkthrough — covers everything end to end: scaffold, install k6, smoke test, average-load profile, JSON output, three charting options, percentile interpretation table, and committing the milestone.
  • Navbar trim. Dropped Stack Selector and Tutorials from the top nav (still reachable from the sidebar and search). Replaced with the AI Integration link as a top-level highlighted item.
v3.23.0May 29, 2026

Security & deploy hardening release. Three threads in one ship: (1) the deploy-day fixes uncovered by a real production build on --single --vite; (2) the React/Vite UI primitives every Grit project ends up writing by hand; (3) a full pass against the OWASP Top 10:2025 with code-level defences and a documented testing methodology. Two new docs pages — /docs/security and /docs/testing — are the audit checklist clients will walk with you.

Deploy reliability

  • cron.Start(cfg, cache) ships. The single-app main.go was importing a function that didn't exist; project wouldn't compile out of the box. The new helper wraps the existing Scheduler and returns (*Scheduler, error) so callers can stop it on shutdown.
  • asynq worker now actually starts. The single-app main.go was queueing jobs without ever starting jobs.StartWorker, so the token-cleanup task and every email/SMS/cron job sat in Redis forever. Worker startup + graceful shutdown wired in.
  • SPA fallback no longer loops behind reverse proxies. c.FileFromFS("index.html", ...) triggered http.FileServer's canonical-URL 301 rule and ping-ponged forever behind Traefik / Cloudflare (ERR_TOO_MANY_REDIRECTS). The scaffold now pre-reads index.html once and serves via c.Data().
  • UUID primary keys no longer 401 every request. GORM's db.First(&user, id) shorthand assumes an integer PK; with the scaffold's UUID-string PK Postgres rejected it with "trailing junk after numeric literal". All eight call sites (auth middleware + UserHandler + TOTP) switched to db.Where("id = ?", id).First(...).
  • Single-app layout cleaned up. main.go moved to the project root so //go:embed all:frontend/dist resolves on a fresh clone; a placeholder frontend/dist/index.html ships so go build works before pnpm build; apps/api/Dockerfile + the multi-app docker-compose.prod.yml are no longer generated in --single mode (Dokploy was auto-detecting them and failing). A new root Dockerfile pins pnpm to 9.15.0, chowns before USER (fixes Sentinel/Pulse "out of memory (14)" SQLite errors).
  • Auto-migrate + first-boot seed. Single-app main.go now runs models.Migrate(db) on startup (gate via AUTO_MIGRATE=false) and seeds when the users table is empty (gate via AUTO_SEED; off by default in production). Fresh-deploy → working-login is a single command.
  • Vite scaffold fixespostcss.config.cjs (was .js, broke ESM package.json); api.ts uses import.meta.env.VITE_API_URL instead of process.env.NEXT_PUBLIC_API_URL; vite-env.d.ts declares the type so tsc --noEmit stops erroring; navbar/footer use TanStack Router's Link + useRouterState instead of next/link / usePathname; @tanstack/router-cli in devDeps + a postinstall hook so routeTree.gen.ts exists on a fresh clone.

Vite UI primitives (every project ends up writing these)

  • lib/auth.ts — handles the actual {data:{user, tokens:{access_token, refresh_token, expires_at}}} envelope, persists tokens, exports login / register / me / refresh / logout / clearAuth. Handles the TOTP-challenge response shape too.
  • lib/api.ts — auto-attaches Authorization: Bearer, transparently retries once on 401 via /api/auth/refresh. Single-flight refresh so a burst of 401s doesn't fan out into N refresh calls.
  • ConfirmDialog + useConfirm — Promise-based confirm: const ok = await confirm({ title, message, tone: 'danger' }). Esc cancels, Enter confirms.
  • MoneyInput — Intl.NumberFormat thousands separators, prefix slot, value: number | null.
  • Combobox — keyboard-friendly (Arrow Up/Down/Enter/Esc), filter on label + sublabel.
  • SessionExpiryMonitor — decodes JWT exp, shows a Stay / Logout modal 30s before expiry. Stay calls refresh().
  • StatusBadge<TStatus> — typed status → tone (success / warning / danger / info / neutral) with a default tone map for paid / pending / overdue / etc.
  • StatsRow — list-page stat cards (label, value, sub, icon, tone).

OWASP Top 10:2025 hardening

Every fresh grit new project now ships defences for every category by default. The new Security Guide walks each one category-by-category.

  • A01 IDOR — internal/authz · authz.MustOwn(c, db, dest, id) returns 404 (not 403) on every failure so existence isn't leaked through error-message differences. authz.RequireRoles("admin") middleware for admin routes.
  • A01 SSRF — internal/safefetch · drop-in safefetch.Get(ctx, url) validates scheme + host pre-flight AND re-checks the resolved IP at TCP-connect time via net.Dialer.Control — closes the DNS-rebind TOCTOU. Blocks loopback, RFC1918, link-local, CGNAT (100.64/10), AWS IMDS (169.254.169.254 + fd00:ec2::/32), metadata.google.internal.
  • A02 — SecurityHeaders middleware extended · strict Content-Security-Policy (default-src 'self' + script allowlist + frame-ancestors 'none' + object-src 'none'), plus Cross-Origin-Opener-Policy and Cross-Origin-Resource-Policy. Skipped on /docs, /studio, /sentinel, /pulse which serve vendored UIs.
  • A03 Supply chain · .github/dependabot.yml (Go modules + npm + GitHub Actions, weekly) and .github/workflows/security.yml running govulncheck + pnpm audit (high+) + CodeQL Go/JS on every PR and weekly.
  • A01-adjacent CSRF — middleware.CSRF · double-submit-cookie defence for cookie-auth routes (OAuth flow). SameSite=Strict on the token cookie.
  • A09 — middleware.LogSecurityEvent · typed event constants for login success/failure, logout, password change, TOTP enable/disable, role change, account lock, authZ denial. Rides the existing tamper-evident ActivityLog hash chain.

Performance & security testing methodology

  • k6 suite in tests/k6/ — all six test types from the testing course (smoke / average-load / stress / spike / soak / breakpoint) share one user journey in lib/common.js. SLO-aligned thresholds; smoke + average-load suitable as a CI regression gate.
  • New /docs/testing page — k6 install + reading results, the 5-phase pentest methodology, the attack catalogue with curl one-liners against a Grit app (IDOR / SQLi / XSS / SSRF / brute-force / misconfig), CVSS scoring + audit-report structure.
  • Sentinel and Pulse issues filed for the cross-project improvements this release uncovered: Sentinel #2 CSP report endpoint, #3 SSRF guard, #4 user-scoped rate limit + CAPTCHA, #5 CVSS finding model + alerts. Pulse #1 p50/p95/p99 + SLO alerts, #2 N+1 detector, #3 USE method dashboard, #4 k6 timeline + flame graphs.
v3.22.0May 2, 2026

Performance hardening release. A senior-level audit of every scaffold template found 27 issues — the 10 critical and high-impact ones are fixed. Apps built with Grit should now show materially lower CPU burn under sustained load.

Critical fixes

  • ActivityLogger middleware — was spawning a fresh goroutine per request, each blocking on a row-level FOR UPDATE lock for the audit hash chain. At 10k req/s the old design created 10k goroutines all serializing on the same lock. Replaced with a bounded channel (4096) + single writer goroutine. The single-writer design eliminates the lock entirely (chain ordering is sequential by construction); the bounded channel caps memory + goroutine count under traffic spikes. Drops on overflow rather than OOM, with a new AuditDroppedCount() helper for monitoring saturation.
  • audit.VerifyChain — was loading the entire activity_log table into memory before scanning. At 1M rows that's 250MB+ heap, instant OOM at 100M. Now walks in chunks of 1000 rows with a cursor on (created_at, id), honours context cancellation, and the integrity endpoint passes a 60-second deadline so a runaway scan can't hold the connection forever.

High-priority fixes

  • flags.Engine.evaluate — copies the flag struct under RLock then releases before doing all decision logic (date checks, allowlist scans, bucketing, JSON parsing). Cuts lock-hold time from milliseconds to nanoseconds on the flag-check hot path.
  • Cache middleware — SHA-256 cache keys swapped for FNV-1a. ~50× faster on the hot path of every cacheable request, no correctness loss (cache keys don't need cryptographic strength). responseCapture switches []byte append to bytes.Buffer — 3 allocations instead of one per Write chunk.
  • Generated service queriesUpdate dropped the redundant third First() after Updates() (Updates mutates the loaded struct in place); Delete dropped the preflight First() (GORM's Delete is atomic + RowsAffected reveals existence). 2 queries saved per generated CRUD op.
  • Generated Export handler — was loading every matching row with Find(&items); now uses FindInBatches in chunks of 1000. CSV exports stream directly to the response writer (true streaming, constant memory). XLSX still buffers because excelize has no streaming API, but the scan is chunked so we don't hold the entire result set in one slice. New export.CSVRows() helper for header-less subsequent batches.

Medium fixes

  • Webhook Replayretry_count increment is now atomic via gorm.Expr("retry_count + ?", 1). Two concurrent replays of the same event no longer race to write the same +1 result.
  • Flags bucketFor for anonymous users crypto/rand.Read instead of time.Now().UnixNano() % 100. The old approach was biased toward recent buckets under high QPS.

Skill file: Performance & Production Hygiene section

The .claude/skills/grit/SKILL.md that grit new generates now includes a dedicated Performance & Production Hygiene section. AI assistants helping users build apps will see explicit hot-path rules, DB query rules, background job rules, logging rules, and memory rules — including which framework primitives are already audited (so they know not to reintroduce the patterns this release just fixed). Examples:

  • Never spawn unbounded goroutines per-request — use a buffered channel + fixed worker pool (the ActivityLogger pattern).
  • Never hold a mutex across slow operations — read shared state, copy what you need, release, then do the work (the flags.evaluate pattern).
  • Never load a whole table into memory — use paginate.List, FindInBatches, or cursor walks (the VerifyChain pattern).
  • Never use time.Now().UnixNano() % N for randomness — biased by call frequency; use crypto/rand.
v3.21.0May 2, 2026

Feature flags + A/B testing baked into the framework (#46). No LaunchDarkly bolt-on, no PostHog SaaS dependency — the engine, the model, the admin endpoints, and the realtime push all ship in every scaffolded API.

Usage

if flags.IsEnabled(c, "new_dashboard") {
    // … render the new dashboard
}

switch flags.Variant(c, "checkout_redesign") {
case "control":   /* old flow */
case "variant_a": /* new flow */
case "variant_b": /* alternate new flow */
}

Mechanics

  • FeatureFlag.Rules JSON holds rollout_percentage, allowlist_user_ids, blocklist_user_ids, enabled_from, enabled_until, variants.
  • All flags load into an in-memory cache at boot. A background goroutine refreshes every 30s; admin writes trigger an immediate refresh. Flag checks never hit the DB.
  • Sticky bucketing: SHA-256(user_id || ":" || flag_name) % 100. A user always lands in the same bucket for a given flag — no flicker between sessions.
  • Allowlist always passes (bypasses the percentage roll). Blocklist always denies. Both run before the percentage check.
  • A/B mode kicks in when Rules.Variants is non-empty. Variant() returns the bucket-mapped variant string. Sticky per (user, flag).

Realtime updates

When a flag is created / updated / deleted, the engine refreshes its cache and broadcasts a "flag.updated" realtime event over the v3.12 WebSocket hub. Frontend subscribers can invalidate their cache and refetch — flag changes propagate in <1s across all connected clients.

Admin endpoints

  • GET /api/admin/flags — paginated list (searchable on name + description, sortable on name / created_at / enabled).
  • POST /api/admin/flags — create. Name is unique + immutable.
  • PUT /api/admin/flags/:id — update description / enabled / rules. Bumps Version (the v3.14 optimistic-lock column).
  • DELETE /api/admin/flags/:id — remove + invalidate cache.
  • GET /api/admin/flags/:id/exposures — variant counts for the rollout-health view: [{ "variant": "enabled", "count": 4231 }, ...].

Fail-closed semantics

  • Unknown flags return false. A typo in a flag name never accidentally enables a feature.
  • Misconfigured Rules JSON also returns false — the engine never panics on bad data.
  • Anonymous users (empty user_id) get a random bucket per request + are not exposure-tracked. For sticky anonymous flags, pass a stable identifier (session ID, device ID).

Pairs with the v3.16 activity log + v3.19 hash chain — every flag change is auditable, signed, and tamper-evident. SOC2-ish flag governance for free.

v3.20.0May 2, 2026

Webhook receiver framework (#57). Wiring up Stripe / GitHub / WhatsApp / any HMAC-signed inbound webhook is now <10 lines of app code. Signature verification, idempotency, failed-handler replay — all framework concerns now.

The shape

// In your app boot (e.g. internal/webhooks/handlers.go)
func init() {
    webhooks.Register("stripe", webhooks.Provider{
        SecretEnv: "STRIPE_WEBHOOK_SECRET",
        Verify:    webhooks.StripeVerifier,
        Extract:   webhooks.StripeExtractor,
    })

    webhooks.On("stripe", "invoice.paid", func(ctx context.Context, e *models.WebhookEvent) error {
        // … process the event
        return nil
    })
}

The framework already mounted POST /webhooks/:provider in routes — the path param picks the registered provider. No per-provider routing code in your app.

Pipeline

  1. Route hits → look up provider (404 if unknown).
  2. Read raw body + headers.
  3. provider.Verify(secret, body, headers) — 401 on signature mismatch.
  4. provider.Extract(body, headers) returns (eventType, externalID).
  5. Insert into webhook_events — UNIQUE on (provider, external_id) means duplicate deliveries become status=skipped no-ops.
  6. webhooks.Dispatch(ctx, event) runs the registered handler for (provider, eventType), falling back to a catch-all "" handler if no specific match.
  7. Handler success → status=processed; handler error → status=failed + handler_error recorded. Provider always gets 200 once we persisted the event, so retries don't hammer.

Shipped verifiers

  • HMACVerifier(header) — generic hex HMAC-SHA256 in a named header. Most simple partners use this.
  • StripeVerifier — Stripe's t=...,v1=... scheme with 5-minute replay tolerance.
  • GitHubVerifier — GitHub's X-Hub-Signature-256: sha256=... header.
  • Roll your own VerifyFunc for anything else — it's just func(secret string, body []byte, headers map[string]string) error.

Shipped extractors

  • JSONFieldExtractor("type", "id") — pulls top-level fields from the JSON body. The most common shape (Stripe-style envelopes).
  • GitHubExtractor — reads X-GitHub-Event + X-GitHub-Delivery headers.

Admin endpoints

  • GET /api/admin/webhooks?provider=stripe&status=failed — paginated list with the standard envelope. Filters: provider, status.
  • POST /api/admin/webhooks/:id/replay — re-runs the handler for an existing event. Increments retry_count and records the new outcome. Use this after a deploy fixes a handler bug.

Pairs naturally with the v3.10 idempotency middleware — both are "safe replay" primitives, just on different sides of the network. Outbound retries reuse Idempotency-Key; inbound duplicates dedupe on (provider, external_id).

v3.19.0May 2, 2026

Tamper-evident audit log via append-only hash chain (#48). Builds on the v3.16 ActivityLog — every row now carries PrevHash + Hash columns where Hash = SHA-256(PrevHash || canonical(row)). Mutating any row breaks the chain on the next verification pass.

The chain

  • Genesis row has PrevHash = ""; every subsequent row references the previous row's Hash.
  • Hash input is the stable canonical form of the audit-relevant fields (user_id, method, path, status, payload digest, IP, UA, duration, created_at unix-nano). ID + PrevHash + Hash themselves are not in the canonical form — they're either random (ID) or derived (Hash, PrevHash).
  • Insert uses FOR UPDATE lock on the latest row inside the same transaction, so concurrent writes serialize cleanly without forking the chain.

The package

New internal/audit ships these:

  • audit.Canonical(entry) — stable JSON bytes for hashing.
  • audit.ComputeHash(prevHash, canonical) — runs SHA-256 over prevHash || canonical; returns hex.
  • audit.AppendChained(db, entry) — atomic insert with chain lock. The middleware uses this; you can call it from anywhere.
  • audit.VerifyChain(db) — walks every row in (created_at, id) order and recomputes hashes. Returns ChainStatus with the first mismatch (broken_at_id + expected vs got + message).

The endpoint

GET /api/admin/activity/integrity

→ { "valid": true, "total_entries": 12345 }

→ { "valid": false, "broken_at": 47, "broken_at_id": "uuid",
    "expected": "abc123...", "got": "def456...",
    "message": "hash mismatch — row was modified, deleted, or inserted out of order" }

Wire this to a nightly cron + alerting webhook for free SOC2-ish audit monitoring. Run it on-demand from a settings page when staff need the current chain state.

What this defends against

  • Direct SQL UPDATE / DELETE on activity_logs — the most common attack vector (DBA covering tracks).
  • Out-of-band insertion of forged history.

What it does NOT defend against

  • Compromise of the running server itself — an attacker with code execution can rewrite the entire chain.
  • External anchoring (publishing the daily root hash to a public ledger like a tweet, a transaction, or a Sigstore log) is the follow-up — flagged in #48 as bonus material, not shipped here.

Verification cost: O(n) — about 2–3 seconds per million rows on a warm cache. The middleware insert is still fire-and-forget so audit DB latency never blocks the response path; chain failures log instead of cascading.

v3.18.0May 2, 2026

PDF generation module (#13). Every scaffolded API ships internal/pdf/ with Grit-styled section helpers + a worked RenderInvoice template. Pure Go, no Chromium / wkhtmltopdf native dependencies.

The Doc primitives

pdf.New() returns a *Doc preconfigured with Helvetica + 20mm margins + A4 portrait + Grit blue accent. Embeds the underlying *fpdf.Fpdf so the full library is available when helpers don't fit.

  • Header(title, subtitle) — accent-colored 22pt title + muted-gray subtitle line.
  • KV(label, value) + TwoColumnKV(...) — small-caps label + body value pairs.
  • Table(headers, rows, widths, aligns) — light gray header row, plain data rows, configurable widths + alignment per column.
  • Totals([]TotalLine) — right-aligned totals stack; the bold line gets accent coloring + slightly larger size for the grand total.
  • Notes(text) — labeled multiline section, skipped when empty.
  • Footer(text) — centered italic 25mm above the page bottom.
  • d.Bytes() finalizes and returns the PDF byte slice ready to stream to c.Data(200, "application/pdf", b).

RenderInvoice — worked example

pdf.RenderInvoice(pdf.Invoice{
    Number:    "INV-202605-0001",
    IssueDate: time.Now(),
    DueDate:   time.Now().Add(14 * 24 * time.Hour),
    BillTo:    pdf.Party{Name: "Abu Seal", Contact: "abu@example.com"},
    Items: []pdf.LineItem{
        {Description: "Office rent — June", Quantity: 1, UnitPrice: 1500000, Total: 1500000},
        {Description: "Service charge",      Quantity: 1, UnitPrice:  120000, Total:  120000},
    },
    Subtotal: 1620000, Total: 1620000,
    Currency: "UGX",
    Notes:    "Pay by mobile money: +256...",
})

Returns ([]byte, error) — wire it to a handler:

func (h *InvoiceHandler) PDF(c *gin.Context) {
    inv, _ := h.Service.GetByID(c.Param("id"))
    bytes, err := pdf.RenderInvoice(toInvoice(inv))
    if err != nil { respond.Internal(c, err); return }
    c.Header("Content-Disposition", `attachment; filename="` + inv.Number + `.pdf"`)
    c.Data(200, "application/pdf", bytes)
}

Copy invoice.go as a starting point for receipts, leases, statements, quotes — the same primitives compose all of them. Add github.com/go-pdf/fpdf v0.9.0 dependency lands automatically in scaffolded go.mod.

v3.17.0May 2, 2026

Quality-of-life bundle. Four GitHub issues closed: #12, #31, #35, #43.

grit init — #35

New CLI command writes CLAUDE.md + AGENTS.md to the current directory. Both files carry the framework's hard rules (Forms / Frontend stdlib / Data / Backend / Resources / Sync / Auth) so contributors and AI assistants get the conventions right on first PR. Skips existing files unless --force is passed; re-run with --force after a major framework upgrade to refresh.

Verbose AutoMigrate — #31

Migrate() now snapshots ColumnTypes before and after each AutoMigrate call and logs a diff:

================================================================
DATABASE MIGRATION — 8 model(s) registered
================================================================
  + created models.Building
  ~ models.User — added 2 column(s): is_vip, vip_notes
----------------------------------------------------------------
Migration done — 1 created, 1 altered (+2 column), 6 unchanged.
================================================================

Silent migrations are gone. Also fixes a pre-existing bug where Migrate skipped already-existing tables — so columns added to a model never actually landed in the DB. Now they do.

Cursor-based pagination — #43

  • paginate.List gains opt-in cursor mode via Config.CursorMode: true. Response carries Meta.NextCursor + Meta.HasMore instead of Page/Pages.
  • Detects HasMore by fetching PageSize + 1 rows — no separate count query needed.
  • Cursor is opaque base64 of (sort_value, id) so pages stay stable when rows insert mid-pagination. Works with any sort field; extracts the value via reflection on the last row.
  • Total count opt-in via Config.IncludeTotal — costs an extra COUNT(*), leave off unless your UI shows a "X of Y" indicator.
  • Offset mode stays the default for back-compat; new resources can flip the flag.

Generator quality — #12

The remaining tag-default heuristics from issue #12:

  • URL fields (suffix _url + named url / image / avatar / thumbnail / logo / cover / icon / banner / photo) get size:500 instead of size:255. UTM-tagged links and signed S3 URLs blow past 255 in the wild.
  • Long-text fields named description / notes / content / body / summary / bio / details / comment / comments / message get type:text.
  • Money fields on float type (suffix _amount / _price / _total / _cost / _fee / _balance / _rent / _salary / _wage / _value / _revenue / _deposit + named amount / price / total / cost / fee / balance / subtotal) get type:decimal(12,2) for fixed-precision storage. No more 1.99 + 0.01 = 1.9999999.
v3.16.0May 2, 2026

Three coherent admin-operations features at once: CSV/Excel export per resource (#15), activity audit log middleware (#32), and the apiErrorMessage frontend helper (#27).

CSV / Excel export per resource — #15

  • New internal/export package: CSV(w, items, opts) and XLSX(w, items, opts) with a typed Column{Header, Field, Format} config. Field uses dot-notation for associations ("Tenant.Name").
  • Format strings: "currency:UGX", "date:2006-01-02", "datetime", "bool". Empty string falls back to fmt.Sprintf("%v").
  • Resource generator now emits an Export(c *gin.Context) handler method on every new resource, with columns derived from the field list. Routes inject GET /api/<plural>/export automatically.
  • Honours the same search param as List, so users can export a filtered subset.
  • Adds github.com/xuri/excelize/v2 v2.8.1 to scaffolded go.mod.

Activity audit log — #32

  • New models.ActivityLog with user_id + method + path + status + payload digest (sha256, not raw body) + IP + user-agent + duration. UUID PK; created_at indexed for time-range queries.
  • New middleware.ActivityLogger(db) mounted on every protected mutation route. Skips safe methods + non-2xx responses + unauthenticated requests.
  • Insert is fire-and-forget (goroutine). Audit DB latency never blocks the response path; if the DB is down the entry drops rather than failing the request.
  • New endpoint GET /api/admin/activity (admin-only) with paginate.List filtering by user_id, method, and path prefix. Drop in any audit-log UI.

apiErrorMessage helper — #27

  • Three helpers in packages/shared/types/api.ts: apiErrorMessage(err, fallback?), apiErrorCode(err), apiErrorFields(err).
  • Walks the standard envelope chain (response.data.error.message) plus axios err.message plus a fallback so toast.error(apiErrorMessage(err)) is always meaningful.
  • apiErrorCode returns the envelope's code string ("VALIDATION_ERROR", "VERSION_CONFLICT", etc.) for branching logic.
  • apiErrorFields surfaces per-field validation details so forms can highlight specific inputs.
  • New internal/respond package on the server side too: respond.NotFound / Validation / Forbidden / Conflict / Internal for handlers, replacing ad-hoc inline c.JSON(500, gin.H{...}).
v3.15.0May 2, 2026

Frontend stdlib + form primitives. Closes seven GitHub issues at once (#19, #20, #21, #22, #23, #33, #34). Every primitive lifted from real Grit-built business apps.

Format helpers (lib/format.ts) — #33

  • formatCurrency(amount, currency?) — locale-aware, no-decimal mode for UGX / JPY / KRW / RWF / TZS / VND.
  • formatDate(value, fmt?) — token formatter (yyyy / MMMM / MMM / MM / dd / HH / mm / ss). Default "MMM d, yyyy".
  • formatDateTime(value) — "May 2, 2026 · 2:30 PM".
  • humanize("checked_in") → "Checked in".
  • initials("Abu Seal") → "AS".
  • setFormatConfig({ locale, currency }) at boot to override.

<CurrencyField> — #19

Live comma formatting as the user types ("3000" → "3,000"), paste-friendly ("$1,234.56" works), emits raw number to onChange. Optional prefix slot for currency code. Auto-toggles between formatted display (blur) and raw digits (focus) so editing isn't a fight.

<SearchableSelect> — #20

Combobox with typeahead, ↑/↓/Enter/Esc keyboard nav, portaled dropdown (escapes overflow:hidden ancestors), optional clear button. Replaces native <select> for FK fields and any enum with > 5 values.

<DateField> + <DateRangeFilter> — #21

  • <DateField> wraps native <input type="date"> with the standard label/hint/ error chrome — picked the native one for a11y, RTL, and i18n.
  • <DateRangeFilter> — preset chip bar (Today / Last 7 / Last 30 / This month / Last month / Last 90 / This year / All) + custom-range fallback.
  • presetRange("last90") helper exposed for non-UI uses.

<Drawer> — #22

Right-edge slide-in panel. Closes on Esc + backdrop + X button. Configurable widths (sm/md/lg/xl). Optional sticky footer slot for the typical Cancel/Save row. Pair with <FormGrid> + <FormActions> from v3.11 for the standard create/edit experience.

<StatusBadge> — #34

Status string → coloured pill. Default map covers paid / active / completed / pending / overdue / cancelled / draft / archived / checked_in / in_progress and friends. Override or extend per app:

setStatusVariants({
  shipped: "info",
  on_hold: "warning",
});

<AppShell> + grouped sidebar — #23

  • New lib/nav-config.ts — single source of truth for sidebar sections. Adding a new section is a one-line config edit.
  • components/layout/sidebar.tsx rewritten as a config-driven grouped sidebar (section title + items with icons + optional badges).
  • New components/layout/app-shell.tsx — bundles TitleBar + Sidebar + Topbar + scrollable content + Cmd/Ctrl-K command palette in one component. Wrap your dashboard Outlet with this.
v3.14.0May 2, 2026

Offline-first foundation. Git-style sync model — work locally, click Sync explicitly, resolve conflicts per-field, push one-by-one. Every scaffolded API now has Version-tracked rows + the POST /api/sync/push and GET /api/sync/pull endpoints; every desktop scaffold ships a local SQLite mirror, an outbox with squash semantics, and a title-bar Sync button + conflict-resolution dialog.

Server: versioning + sync endpoints

  • Version int column added to User, Upload, Blog. A BeforeUpdate GORM hook auto-increments on every server-side write. The resource generator emits both on every new model.
  • POST /api/sync/push accepts a batch of changes; each entry includes the version the client believes the server has. On mismatch the response contains VERSION_CONFLICT + the current server state, so the client can drive a merge UI.
  • GET /api/sync/pull?model=X&since=cursor returns every row in the table updated after the cursor, paginated, with a new cursor in the response.
  • New internal/sync/registry.go maps logical table names (e.g. "buildings") to reflect.Type so the handler decodes dynamic payloads. New resources auto-register via // grit:sync marker.

Desktop: sync engine

  • New apps/desktop/sync/ Go package. Opens a local SQLite file under the OS user-config dir on app boot.
  • Three tables: sync_records (local mirror — reads come from here), sync_outbox (pending changes; UNIQUE on (model, entity_id) for squash), sync_cursors (incremental pull positions).
  • Squash semantics: edit a record three times offline → one outbox entry with the final state. delete-after-create cancels both locally without ever hitting the network.
  • Engine.Sync() runs Pull then Push. Push posts the whole outbox in one HTTP call; the response drives per-entry result handling — successes clear from the outbox, conflicts get the server state stashed for the merge UI.

Wails bindings

The frontend talks to the engine through these Wails-bound methods on App:

  • LocalCreate / LocalUpdate / LocalDelete — write-through to local SQLite + outbox.
  • LocalGet / LocalList — read from the local mirror.
  • Sync(tables) — pull listed tables then push the outbox. Returns counts.
  • PendingCount, GetPendingChanges — drive the title-bar badge and the review panel.
  • ResolveConflict(table, entityID, mergedData, serverVersion) — accepts the user's merge for a conflicted entry.

UI

  • Title-bar Sync button with a pending-count badge. Green refresh icon when clean; amber alert + count when there are pending changes.
  • PendingChangesPanel — right-edge drawer listing every outbox entry, split into "Needs review" (conflicts) and "Ready to push". Sync now button at the bottom.
  • ConflictDialog — field-level merge UI. Three columns (Field / Local / Server v_N), per-field click to choose. Apply builds the merged record and calls ResolveConflict.

React hooks

  • usePendingCount() — polls every 2s for the badge.
  • usePendingChanges() — full outbox + refresh function.
  • useSyncMutation(tables) — kicks off a Sync, exposes running/result/error state.
  • useResolveConflict() — applies one merge and refreshes.

Wire format

POST /api/sync/push
{ "changes": [
    { "op": "create", "model": "buildings", "id": "uuid", "version": 0, "data": {...} },
    { "op": "update", "model": "tenants",   "id": "uuid", "version": 5, "data": {...} },
    { "op": "delete", "model": "leases",    "id": "uuid", "version": 3 }
] }

→ { "results": [
    { "ok": true, "new_version": 1 },
    { "ok": false, "code": "VERSION_CONFLICT", "server_version": 7, "server_data": {...} },
    { "ok": true }
] }

Deferred to v3.14.1: React Query offline-aware data hooks (useOfflineList, useOfflineGet, useOfflineMutation) and the resource generator emitting offline-aware frontend hooks. The engine and primitives ship now; ergonomics layer next.

v3.13.0May 2, 2026

New grit generate sequence command produces atomic, gap-free sequential numbers like INV-202605-0001. Pattern lifted from a real Grit-built rental management app — invoice / receipt / order numbering is now a one-liner.

What it generates

  • First invocation only: internal/sequence/sequence.go — a generic counter package with Counter (the GORM-backed row), Config (name + prefix + reset + width), and an atomic Next(db, cfg, t) helper.
  • Every invocation: internal/services/<name>_sequence.go — a typed convenience wrapper. Handlers call e.g. services.NextInvoiceNumber(h.DB, time.Now()) without knowing the prefix or reset cadence.
  • Auto-injects &sequence.Counter{} into the Models() migration slice (idempotent).

Mechanics

  • Counter rows keyed by (name, bucket) where bucket is "YYYYMM" for monthly resets, "YYYY" for yearly, or empty for never. So a monthly counter automatically restarts at 1 on the first call of each new month.
  • Atomic via row-level SELECT FOR UPDATE on Postgres (concurrent callers serialize on the counter row). SQLite serializes writes globally so it's also safe.

Usage

grit generate sequence Invoice
grit generate sequence Order --prefix ORD --reset yearly --width 6
grit generate sequence Receipt --reset never

Flags:

  • --prefix — alphabetic prefix (default: first 3 chars of the name, uppercased)
  • --reset — when the counter resets: monthly (default), yearly, or never
  • --width — zero-padded width of the numeric portion (default 4)

The grit generate report generator (Recharts tabs page + Go ReportService) is deferred to a future release — it needs more design work for the React chart layer than fits a same-day release.

v3.12.0May 2, 2026

Realtime WebSocket hub baked into every API + a desktop client + hooks for subscribing. And a sweep of every remaining numeric ID — UUIDs are now the canonical ID type everywhere in the framework.

Realtime hub (API)

  • New package: internal/realtime/hub.go. One Hub per process; each user can have multiple connections (desktop + mobile + web).
  • Hub.SendToUser(userID, evt), SendToUsers(ids, evt), and Broadcast(evt) let any handler or service push events.
  • Slow-client safe: per-connection 32-message send buffer; when full, that one client's message is dropped — never blocks the entire hub. Slow clients resync on their next REST refetch.
  • New handler: internal/handlers/realtime.go upgrades the request to a WebSocket and registers the client with the hub.
  • Mounted at GET /api/ws?token=<jwt> — query-string auth because browsers can't set custom headers on the WS handshake.
  • Wire format: { type: "<topic>", payload: {...} }. Suggested topics: chat.message.new, notification.new, system.connected, or your own resource.<name>.<verb> namespace.
  • Dependency added: github.com/gorilla/websocket v1.5.3.

Realtime client (desktop)

  • New file: frontend/src/lib/realtime.ts. Singleton client with auto-reconnect via exponential backoff (1s, 2s, 4s, 8s, capped at 15s).
  • Global realtimeBus EventTarget — any component can subscribe.
  • Start from AuthProvider after tokens land, stop on logout.
  • New hook: useRealtimeEvent<T>(type, callback) subscribes to a typed topic and unsubscribes on unmount. Plus useRealtimeAny() for catch-all handlers (debug, toast bar).

ID consistency sweep — UUIDs everywhere

v3.9.1 standardized the User model on string UUID PKs but a long tail of numeric IDs remained in the framework. v3.12.0 cleans them all up.

  • Go scaffold: the prebuilt Blog model in api_blog_files.go swaps from gorm.Model (auto-incr uint) to a string UUID PK with a BeforeCreate hook. Service signatures (GetByID, Update, Delete) and handler param parsing all switch from uint to string.
  • Standalone desktop scaffold (grit new-desktop): User, Blog, and Contact models all switch to string UUID PKs with BeforeCreate hooks. All Wails-bound App methods and underlying service signatures use id string. Frontend mutation typings follow.
  • Shared TS types: User, Upload, and Blog interfaces all use id: string. URL builders in API_ROUTES take id: string. The BlogSchema Zod schema uses z.string().
  • Admin TS: DataTable selection state, generic useResourceItem / useUpdateResource / useDeleteResource mutation typings, RelationshipSelectField single value, MultiRelationshipSelectField array values, and handleDelete callbacks all switch from number to string.

Net effect: UUID is the canonical ID type across the entire framework. Any resource generator output, any scaffolded type, any Go signature — all string UUIDs. No more id: number hiding in some corner.

v3.11.0May 2, 2026

Three new desktop primitive files ship with every --desktop scaffold, lifted from a real Grit-built rental management app. They cover the master-detail layout, form chrome, and filter chips that every CRUD page reinvents — saving ~200 LOC per resource.

components/two-pane.tsx — master-detail layout

  • TwoPane — outer flex container with overflow handling.
  • ListPane — fixed-width (352px) left pane with title + count + new button + searchbar + optional filters slot + scrollable body + optional footer. Toolbar slot for refresh buttons or other actions.
  • ListRow — icon/avatar + title + subtitle + right-side meta. Selected state shows a 2px accent bar on the left edge.
  • DetailPane — right pane with optional header + scrollable content. empty=true renders an EmptyState with the configured title/hint instead.
  • EmptyState, DetailSection (small caps section header), and DetailField (labelled value rows for read views).

components/form.tsx — form chrome

  • TextField, TextAreaField, SelectField — forwarded refs, consistent label/hint/error layout, focus ring, disabled styling. Plug straight into react-hook-form.
  • FormGrid — 1, 2, or 3 columns on >=sm; stacks on small screens.
  • FormSection — small caps title + optional description over a stack of fields.
  • FormActions — Cancel + Submit pair with isPending support (button disables, label flips to "Saving...").

components/filter-chip.tsx — filter chips

  • FilterChip — toggleable pill, active state shows accent background; optional onClear renders an X to clear a single filter; optional count renders a small count badge.
  • FilterBar — horizontal scrollable wrapper. Drop intoListPane's filters slot.

Tailwind tokens

  • Added listpane spacing token (22rem / 352px) to the desktop Tailwind config so w-listpane works.
v3.10.0May 2, 2026

Foundation release for upcoming offline-first work. Every scaffolded API now ships with idempotent-retry semantics; every scaffolded client auto-attaches an Idempotency-Key on mutations; and the desktop scaffold gains a connection-status indicator backed by an API heartbeat.

Idempotency middleware (API)

  • New file: internal/middleware/idempotency.go — wired into routes.Setup as a global middleware.
  • Activates only when the request carries an Idempotency-Key header and the method is POST/PUT/PATCH/DELETE.
  • First 2xx response is cached in Redis for 24 hours, keyed by (method, path, key). Subsequent requests with the same key replay the cached response instead of re-executing the handler.
  • Errors (4xx/5xx) are intentionally not cached — clients can retry transient failures with the same key.
  • Sets Idempotent-Replayed: true response header on cache hits so clients can distinguish replays from fresh executions.

Client-side header injection

  • Desktop, Expo, web, and admin clients all auto-attach a UUIDv4 Idempotency-Key on unsafe methods via the request interceptor.
  • The 401-refresh path now reuses the same key when re-issuing a request after a token refresh — so a token expiring mid-write can never double-create.

Online-status hook (desktop)

  • New hook: useOnlineStatus() at frontend/src/hooks/use-online-status.ts.
  • Combines navigator.onLine (cheap pre-check) with a 15-second heartbeat to /api/health (the truth signal). Returns { isOnline, lastCheckedAt }.
  • Heartbeat times out after 5s so a sleeping laptop surfaces as offline instantly on wake.
  • The title-bar gains a ConnectionIndicator — small green/amber dot reflecting API reachability. Hover for last-checked timestamp.

This is the foundation for the offline-first scaffold landing in a later v3.x release — write-queues, optimistic updates, and last-write-wins conflict resolution all need stable idempotency keys to be safe.

v3.9.2April 25, 2026

Every grit generate resource run now emits a List handler that is ~15 lines instead of ~55. The page / sort / search boilerplate moved into a shared internal/paginate package that ships with every scaffolded API — one source of truth for clamping, whitelisting, and search. Addresses issue #14.

New paginate package

  • paginate.List[T](query, paginate.Bind(c), paginate.Config{...}) — typed, generic helper that runs search, sort, filter, and pagination against any *gorm.DB query.
  • paginate.Bind(c) reads page, page_size, search, sort_by, sort_order from the Gin query, clamps page to ≥ 1 and page_size to [1, 100].
  • paginate.Config whitelists sortable columns and declares the searchable column set — requests for columns outside the whitelist fall back to created_at desc.
  • paginate.Result[T] returns the canonical { data, meta: { total, page, page_size, pages } } envelope — matches the existing API response format exactly.

Generator update

  • The emitted List handler now delegates to paginate.List. Every generated resource gets the same clamping, whitelisting, and UUID-safe search behavior — no per-resource drift.
  • Searchable column selection uses IsSearchable() (text / string / slug / richtext only), so FK UUID columns are no longer accidentally included in ILIKE search — a leftover rough edge from issue #12.
v3.9.1April 24, 2026

Patch release fixing compilation and consistency bugs in v3.9.0. Every freshly scaffolded project (including --mobile --desktop) and every grit generate resource run now produces Go code that builds cleanly on the first try. Thanks to issue #9, #10, #11, and #12.

Scaffold fixes

  • Missing imports: added "log" to config.go, "gorm.io/gorm/logger" to user.go, "net/http" to middleware/logger.go.
  • Stray package prefix: removed handlers. qualifier on IsTrustedDevice (same-package call).
  • User ID type consistency: normalized UserID and UploadID to string UUIDs across 2FA models, auth service, TOTP handler (c.GetString("user_id") replaces c.GetUint), jobs package, and upload handler.

Desktop scaffold fixes

  • keychain.go moved from internal/ to the top level (the subdirectory file was declaring package main, which Go rejects).
  • go.mod module path fixed from <project>/apps/api/apps/desktop to <project>/apps/desktop.

Resource generator fixes

  • Service signatures take id string instead of id uint -- matches the UUID string PK the models have always emitted.
  • Handler FK fields, handler M2M arrays, TS interface FK fields, and TanStack hook ID types all switched to string (were uint / number).
  • Initialism-aware toPascalCase / toSnakeCase: owner_idOwnerID (was OwnerId), image_urlImageURL (was ImageUrl), api_keyAPIKey. Round-trips correctly (snake → pascal → snake).
  • Zod schemas now emit snake_case field names matching the Go handler's JSON tags (previously emitted camelCase, causing validation andShouldBindJSON mismatches).
  • Zod FK and M2M validators use z.string().uuid() instead of z.number().int().
  • FK columns generate with gorm:"size:36;index" (matches UUID PK width).
v3.9.0April 15, 2026

New: --desktop flag

  • Desktop + mobile + API in one monorepo grit new myapp --mobile --desktop scaffolds a complete multi-client SaaS: Go API shared by an Expo mobile app AND a Wails desktop app. All three share the same packages/shared types and schemas.
  • Wails as a thin client — The new desktop app is a frameless Wails window that calls the shared API over HTTP. No embedded Go business logic, no local SQLite. Wails bindings are used only for native OS features: window controls, file dialogs, and OS keychain (macOS Keychain, Windows Credential Manager, Linux Secret Service) for JWT storage.
  • Distinct from grit new-desktop — The standalone offline-first desktop scaffold (grit new-desktop) is unchanged. --desktop is a new, separate capability for always-online multi-client apps.

Premium Desktop UX

  • Platform-aware window chrome — macOS traffic lights on the left, Windows/Linux controls on the right. Detected at runtime viaGetPlatform() Wails binding.
  • Command palette (⌘K) — every scaffolded desktop app ships with a Raycast/Linear-style command palette. Searchable navigation + actions with keyboard-first UX.
  • Fixed 240px sidebar — not collapsible. Desktop windows are wide enough; collapse toggles are a web pattern.
  • Global keyboard shortcuts useShortcuts() hook with defaults: ⌘K palette, ⌘, settings, ⌘L logout, Esc to close.
  • More negative space — content padding is 32px(vs web's 24px) for long focus sessions. Subtler shadows (OS chrome already provides elevation).

Style Guide

  • New §14.5 Desktop App Patterns section inGRIT_STYLE_GUIDE.md covering window chrome, sidebar (not collapsible), topbar, command palette, keyboard shortcuts, OS keychain integration, typography (tighter than web), and do's & don'ts (no breadcrumbs, no header banners, no web-style autoplay).

Usage

grit new myapp --mobile --desktop --next
# apps/api + apps/web + apps/expo + apps/desktop

grit new myapp --desktop --triple
# apps/api + apps/web + apps/admin + apps/desktop

grit new myapp --api --desktop
# apps/api + apps/desktop (minimal)
v3.8.0April 11, 2026

Design System

  • GRIT_STYLE_GUIDE.md — First official style guide for all Grit-scaffolded projects. Premium Minimal aesthetic (Linear / Vercel school), Grit purple #6C5CE7 primary, Onest font. Covers typography, color palette, spacing, shadows, every component spec (buttons, inputs, cards, tables, modals), auth page rules, CLI scaffolding design, admin panel patterns, email templates.

Admin Layout

  • Topbar refactor — Moved sidebar collapse toggle to top-left of the topbar (next to mobile menu button). Moved theme toggle, notifications bell, and enhanced user menu to the top-right cluster alongside search. The sidebar now contains only navigation. Matches modern dashboard patterns (Linear, Vercel, Raycast).
  • Enhanced user menu — Dropdown now shows User Activity, Settings, Billing, and Log out sections with a user name/email header.

PageHeader Component

  • Consistent page headers — New <PageHeader /> component at components/layout/page-header.tsx with title, description, breadcrumbs, actions slot, and a 4-card stats grid. Every generated resource page auto-includes it.
  • Auto-generated stats cards — Resource pages now ship with 4 default stat cards (Total, This Week, This Month, Updated Recently) fetched from the API. Override via defineResource({ stats: { cards: [...] } }) or disable with stats: false.

Auth Pages

  • New centered auth variantgrit new myapp --style centered scaffolds Linear-school single-card auth pages (login, sign-up, forgot-password). ~420px card on a subtle radial gradient background. The original split-screen design remains the default (unchanged).
v3.7.0April 3, 2026

Security

  • Security headers middleware — New SecurityHeaders() middleware adds X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, and HSTS (when HTTPS detected) on every response.
  • Max body size middleware — 10MB default limit returns 413 on exceed.
  • JWT secret validation — Warns if JWT_SECRET is shorter than 32 characters.
  • Sentinel WAF — Now runs in ModeBlock in production (was always ModeLog). Development keeps ModeLog.

Performance

  • GORM AutoMigrate silence — Migration now uses a logger.Silent session to suppress schema inspection SQL noise. Fixes issue #8.

Web App Auth

  • Auth pages for the web app — The web app (apps/web) now ships with its own auth pages: login, register, forgot-password, OAuth callback. Previously only the admin panel had auth. This is critical for e-commerce and SaaS where end users log in on the web app, not the admin.
  • useAuth() hook — React Query + js-cookie token management with AuthProvider context wrapping the web app.

Mobile (Expo)

  • Major Expo scaffold upgrade — 4 tabs (Home, Explore, Profile, Settings) instead of 2. All forms use react-hook-form + zod. Home screen with stat cards and pull-to-refresh. Explore screen with search and category discovery. Settings with SectionList. Profile with display/edit mode.
  • OAuth in mobile — Google OAuth via expo-web-browser with deep-link callback handling.
  • New Expo dependencies — react-hook-form, @hookform/resolvers, zod, expo-image, expo-haptics, expo-web-browser. Splash screen config in app.json.
v3.6.0March 27, 2026

Features

  • Scaffold into current directorygrit new . and grit new ./ now scaffold into the current directory instead of creating a subfolder. Infers the project name from the folder name. Also auto-detects when the current directory name matches the project name.
  • --force flag — Allows scaffolding into non-empty directories. Useful when a repo was cloned first (with README, .git, LICENSE) before scaffolding: grit new . --triple --vite --force.
  • --here flag — Explicit alternative to grit new . for in-place scaffolding.
  • 30 standalone courses — Added 20 new courses to the learning platform (42 total across 3 tracks + 20 standalone). Topics include testing, GORM mastery, WebSockets, Stripe payments, blog/CMS, CI/CD, middleware, and the 100-component UI registry.

Bug Fixes

  • Flags now skip interactive prompt — Running grit new myapp --triple --vite no longer shows the architecture/frontend selection prompt. Flags act as true shortcuts for non-interactive setup.
  • Module path upgrade to /v3 — Fixed go install ...@latest downloading v2.9.0 instead of v3.x. All import paths updated from grit/v2 to grit/v3.
v3.5.0March 26, 2026

Documentation

  • Full docs redesign — Rebuilt the documentation site with a Tailwind CSS-inspired aesthetic. New dark theme (#0b1120), sky-blue accents, cleaner header with backdrop blur, redesigned code blocks with file tabs and line highlighting, and new StepWithCode component for two-column step-by-step guides (text left, code right).
  • Installation page redesigned — Step-numbered sections (01-04) with the new two-column layout, system requirements table, architecture shortcuts, and services grid.
  • Architecture Modes page — Visual cards for all 5 architectures (single, double, triple, API only, mobile) with directory structure trees, features list, ideal use cases, and frontend framework comparison.
  • TanStack Router guide — Complete guide for the TanStack Router frontend option: project structure, routing patterns, comparison table with Next.js, route examples, and admin panel auth guards.
  • New CLI Commands page — Documents grit routes,grit down/up (maintenance mode), and grit deploy. Includes complete command reference table for all 21 CLI commands.
  • Deploy Command guide — Step-by-step deployment pipeline with systemd service unit and Caddyfile examples, flags table.

Improvements

  • Updated skill file with all v3.x architecture modes, frontend options, and new CLI commands.
  • Updated sidebar with new pages: Architecture Modes, New CLI Commands, TanStack Router, Deploy Command.
  • Frontend sidebar section renamed from “Frontend (Next.js)” to “Frontend” to reflect multi-framework support.
v3.4.0March 26, 2026

Features

  • Multi-architecture code generatorgrit generate resourcenow works for all 5 architecture modes and both frontend frameworks. Generates Go model, service, and handler at the correct path (internal/ for single app,apps/api/internal/ for monorepo). Generates React Query hooks and admin resource pages for both Next.js and TanStack Router.
  • grit.json project manifest — Every scaffolded project now includes a grit.json file at the root with architecture andfrontend fields. The generator reads this to determine correct file paths and template variants, eliminating fragile filesystem heuristics.
  • TanStack Router resource generation — When generating resources in a TanStack Router project, creates route files atsrc/routes/_dashboard/resources/ using createFileRoute instead of Next.js app/(dashboard)/resources/ page convention.
v3.3.0March 26, 2026

Features (Goravel-Inspired)

  • grit routes — List all registered API routes in a formatted table. Parses routes.go and shows method, path, handler, and middleware group (public/protected/admin). Works for both monorepo and single app projects.
  • grit down / grit up — Maintenance mode.grit down creates a .maintenance file that triggers the new maintenance middleware, returning 503 for all requests. grit up removes it and resumes normal operation.
  • grit deploy — One-command production deployment. Cross-compiles for Linux, builds frontend, uploads binary via SCP, configures a systemd service, and optionally sets up Caddy reverse proxy with auto-TLS. Supports--host, --domain, --key flags orDEPLOY_HOST/DEPLOY_DOMAIN/DEPLOY_KEY_FILE env vars.
  • Maintenance middleware — All scaffolded projects now include aMaintenance() Gin middleware that checks for a .maintenancefile on every request. Runs as the first global middleware.
v3.2.0March 26, 2026

Features

  • Single app architecturegrit new my-app --single creates a single Go binary that serves both the API and an embedded React SPA. Uses go:embedto bake the built frontend into the binary at compile time. One file to deploy. Dev mode runs Go on :8080 and Vite on :5173 with API proxy.
  • Parameterized API paths — All Go API file generators now useopts.APIRoot() and opts.Module() helpers, enabling the same template functions to generate files for both monorepo (apps/api/) and single app (project root) architectures.

Single App Structure

  • cmd/server/main.go — Entry point with go:embed frontend/dist/* and SPA fallback routing
  • internal/ — Full Go backend (same as monorepo API)
  • frontend/ — React + Vite + TanStack Router SPA
  • Makefilemake dev (parallel servers), make build (single binary)
v3.1.0March 26, 2026

Features

  • TanStack Router frontend scaffold — When selecting TanStack Router (Vite) as your frontend, both the web app and admin panel are now fully scaffolded with Vite + TanStack Router + React Query + Tailwind CSS. Includes file-based routing via @tanstack/router-vite-plugin, API proxy in dev mode, and all the same features as the Next.js scaffold.
  • TanStack Router admin panel — Complete admin panel with TanStack Router: auth pages (login, sign-up, forgot password), dashboard layout with sidebar, resource management (users, blogs) via ResourcePage component, system pages (jobs, files, cron, mail, security), profile page. All existing React components (DataTable, FormBuilder, widgets) are reused with automatic"use client" directive stripping.
v3.0.0March 26, 2026

Features

  • Interactive project creationgrit new my-app now launches an interactive prompt to select your architecture and frontend framework. Power users can skip with flags: --single --vite, --triple --next,--api, etc.
  • 5 architecture modes — Choose the project structure that fits your team:Single (Go API + embedded React SPA, one binary),Double (Web + API Turborepo),Triple (Web + Admin + API Turborepo),API Only (Go backend, no frontend),Mobile (API + Expo React Native).
  • Frontend framework choice — Pick between Next.js (SSR, App Router) and TanStack Router (Vite, fast builds, small bundle, SPA). Available for all architecture modes that include a frontend.

Breaking Changes

  • Options struct refactored — The internal Options struct now uses Architecture and Frontend enum fields instead of boolean flags. Legacy flags (--api, --mobile, --full) still work via the Normalize() migration layer.
v2.9.0March 26, 2026

Features

  • Two-Factor Authentication (TOTP) — Every grit new project now includes a complete 2FA system with authenticator app support (Google Authenticator, Authy, 1Password, etc.). Zero-dependency RFC 6238 implementation with HMAC-SHA1. Includes setup flow with QR code URI generation, 6-digit code verification with ±1 window clock skew tolerance, and seamless integration with the existing JWT login flow.
  • Backup Codes — 10 one-time-use recovery codes generated when enabling 2FA. Each code is individually bcrypt-hashed for storage. Codes can be regenerated at any time (invalidates previous set). Use during login as an alternative to the authenticator app.
  • Trusted Devices — “Remember this device” option during TOTP verification. Sets an HttpOnly cookie with a SHA-256 hashed token stored in the database. Trusted devices last 30 days with sliding expiry (refreshed on each use). Users can revoke all trusted devices from their account.

New Endpoints

  • POST /api/auth/totp/setup — Generate TOTP secret + QR URI (authenticated)
  • POST /api/auth/totp/enable — Verify initial code and activate 2FA
  • POST /api/auth/totp/verify — Verify TOTP code during login (public, uses pending token)
  • POST /api/auth/totp/backup-codes/verify — Use backup code during login
  • POST /api/auth/totp/disable — Disable 2FA (requires password)
  • GET /api/auth/totp/status — Check 2FA status, remaining backup codes, trusted device count
  • POST /api/auth/totp/backup-codes — Regenerate backup codes
  • DELETE /api/auth/totp/trusted-devices — Revoke all trusted devices
v2.8.0March 16, 2026

Features

  • Vercel AI Gateway integration — Replaced the multi-provider AI service (Claude, OpenAI, Gemini with separate API implementations) with Vercel AI Gateway. One API key now gives access to hundreds of models from all major providers through a single OpenAI-compatible endpoint. Models use the provider/model format (e.g. anthropic/claude-sonnet-4-6, openai/gpt-5.4, google/gemini-2.5-pro). Includes automatic retries, fallbacks, spend monitoring, and zero markup on tokens.

Breaking Changes

  • AI environment variablesAI_PROVIDER, AI_API_KEY, and AI_MODEL have been replaced with AI_GATEWAY_API_KEY, AI_GATEWAY_MODEL, and AI_GATEWAY_URL. Update your .env file accordingly. Get your API key from vercel.com/ai-gateway.
v2.7.0March 10, 2026

Features

  • 10 Official Plugins — New grit-plugins ecosystem with drop-in Go packages for common functionality: WebSockets (grit-websockets), Stripe payments (grit-stripe), OAuth social login (grit-oauth), notifications (grit-notifications), full-text search (grit-search), video processing (grit-video), WebRTC conferencing (grit-conference), outgoing webhooks (grit-webhooks), i18n translations (grit-i18n), and PDF/Excel/CSV export (grit-export). Each plugin includes a Claude Code skill file for AI-assisted integration.
  • Claude Code Skills format — Updated the scaffolded AI skill file from a monolithic GRIT_SKILL.md to the official Claude Code skills directory structure (.claude/skills/grit/SKILL.md + reference.md) with YAML frontmatter. AI assistants can now discover and use Grit conventions automatically.
  • Grit UI component registry (100 components) — Expanded from 91 to 100 pre-built components across 5 categories: marketing (21), auth (10), SaaS (30), ecommerce (20), and layout (20).

Documentation

  • New Plugins page — overview of all 10 plugins with installation, environment setup, quick start code, features, and use cases for each.
v2.6.0March 6, 2026

Fixes

  • GORM Studio (Desktop) — Replaced the broken custom HTML studio with the real gorm-studio package. Desktop studio now runs on port 8080 at /studio using Gin + gorm-studio, matching the web scaffold. Auto-opens browser on launch.
v2.5.0March 6, 2026

Features

  • GRIT_SKILL.md — Desktop scaffolds now include a GRIT_SKILL.md file in the project root. This is a comprehensive AI reference (12 sections) covering architecture, CLI commands, resource generation, field types, code markers, golden rules, and common LLM mistakes — so AI assistants can work with the project correctly out of the box.
  • Comprehensive README — The scaffolded README.md now includes a full project walkthrough, “Adding a New Module” guide, supported field types table, customization section (window size, title bar, database, app name), code markers reference, and a ready-to-use AI prompt for building a Task Manager app.

Fixes

  • Dashboard stats cache — Dashboard statistics now update immediately after creating a blog or contact. Changed query keys from ["blogs-stats"] to ["blogs", "stats"] so TanStack Query's prefix matching invalidates dashboard queries when resources are created or deleted.
v2.4.0March 5, 2026

Features

  • Window controls on auth pages — Login and register pages now include minimize, maximize, and close buttons with a draggable title area, so users can move and manage the window before signing in.
  • Show/hide password toggle — All password fields on login and register pages now have an eye icon toggle to reveal or hide the password text.

Fixes

  • Desktop build script — Removed tsc from the frontend build script. TanStack Router's Vite plugin generates routeTree.gen.ts during the Vite build, so running tsc before Vite caused Cannot find module './routeTree.gen' errors.
  • Title bar import path — Fixed the Wails binding import in title-bar.tsx from a 2-level to 3-level relative path.
  • Auth hook file extension — Renamed use-auth.ts to use-auth.tsx so TypeScript handles the JSX correctly.
  • Create resource cache refresh — Blog and contact create pages now invalidate the React Query cache before navigating back, so new records appear in the table immediately.
v2.2.0March 4, 2026

Fixes

  • Desktop auth hook file extension — Renamed the scaffolded use-auth.ts to use-auth.tsx so TypeScript correctly handles the JSX in <AuthContext.Provider>. Previously, grit new-desktop projects would fail to compile with TS1005: '>' expected errors.

Documentation

  • Added Desktop Handbook PDF download links to all 8 desktop documentation pages.
v2.1.0March 4, 2026

Features

  • TanStack Router for desktop — Migrated the desktop frontend from React Router to TanStack Router with file-based routing. Routes are auto-discovered by the Vite plugin — no centralized route registry. Uses createHashHistory() for Wails compatibility and Route.useParams() for type-safe params. Resource generation now creates 5 files (list, new, edit routes + model + service) and performs 10 injections (down from 12).
  • Mobile navigation — Added a hamburger menu to the docs site header, visible below the lg breakpoint. Opens a Sheet sidebar with all navigation links. Auto-closes on link click.
  • CGO-free SQLite — Replaced gorm.io/driver/sqlite (requires CGO) with github.com/glebarez/sqlite (pure Go) in all scaffold templates. Desktop apps now build and run without CGO or a C compiler.
  • 20 Desktop Project Ideas — New project ideas page with 20 ready-to-build desktop app ideas across business, education, healthcare, logistics, and more. Each includes resources, field definitions, and grit generate commands.

Documentation

  • Added TanStack Router explanations to all desktop doc pages: overview, getting started, first app, resource generation, and POS app.
  • Updated LLM Reference, GRIT_SKILL.md, and database docs to reflect TanStack Router and CGO-free SQLite changes.
v2.0.0March 4, 2026

Features

  • Native desktop apps (Wails) — New grit new-desktop command scaffolds a complete desktop application with Go backend, React frontend (Vite + TanStack Router + TanStack Query), SQLite database, JWT authentication, blog and contact CRUD, PDF/Excel export, custom title bar, dark theme, and GORM Studio. Compiles to a single native executable for Windows, macOS, and Linux. See Desktop docs.
  • Desktop resource generationgrit generate resource now works inside desktop projects. Generates Go model, service, and TanStack Router route files (list, new, edit), then injects code into 10 locations (db.go, main.go, app.go, types.go, sidebar.tsx, studio/main.go) using grit: markers. See Desktop Resource Generation.
  • Project type auto-detection — All CLI commands now auto-detect whether you are inside a web (Turborepo) or desktop (Wails) project. No flags needed.
  • grit start for desktop — Running grit start inside a desktop project launches wails dev with hot-reload for both Go and React.
  • grit compile — New command that runs wails build to produce a distributable native binary.
  • grit studio — New command that launches GORM Studio. For desktop projects it starts a standalone server on port 4000. For web projects it opens the browser to the embedded Studio route.
  • grit remove resource for desktop — Removes a previously generated desktop resource, deleting files and reversing all 10 marker injections.
  • Grit UI component registry (91 components) — Every scaffolded web project now includes a shadcn-compatible component registry with 91 pre-built components across 5 categories: marketing (14), auth (10), SaaS (30), ecommerce (20), and layout (18). Install via npx shadcn@latest add from /r endpoints.

Documentation

  • New Desktop (Wails) section — 8 pages covering overview, getting started, first app tutorial, POS app tutorial, resource generation, building/distribution, project ideas, and LLM reference.
  • Updated LLM Reference with complete desktop section: project structure, CLI commands, markers, and architecture comparison.
v1.4.0March 2, 2026

Features

  • Gzip response compression — All API responses are now compressed automatically via a custom Gzip() middleware using the Go standard library compress/gzip at BestSpeed. JSON payloads shrink by 60–80%, reducing bandwidth on paginated list endpoints with zero external dependencies.
  • Request ID tracing — A RequestID() middleware injects a unique X-Request-ID header on every request (echoes the upstream header or generates a nanosecond-based ID). The ID is stored in Gin context and included in every structured log line for end-to-end request tracing.
  • Database connection pool tuning — The scaffold now sets four GORM pool parameters: MaxIdleConns(10), MaxOpenConns(100), ConnMaxLifetime(30m), and ConnMaxIdleTime(10m). This prevents stale connections after network interruptions and avoids connection exhaustion under load.
  • Cache-Control headers on public blog endpoints — The ListPublished handler now returns Cache-Control: public, max-age=300 (5 minutes) and GetBySlug returns Cache-Control: public, max-age=3600 (1 hour). CDNs and edge caches can now serve public blog content without hitting the Go API.

Documentation

  • New Performance page — comprehensive guide to all backend (Go/API) and frontend (Next.js) performance optimisations that ship with every Grit project out of the box. Covers Gzip, Request ID, connection pool, Cache-Control, presigned uploads, background jobs, Redis caching, Server Components, ISR, React Query, next/image, Turborepo, and code splitting.
  • New Complete LLM Reference page — a dedicated machine-readable guide that teaches AI assistants everything about Grit: project structure, all CLI commands, every field type, code patterns, API response format, code markers, naming conventions, all batteries, performance features, and the golden rules that must never be broken.
v1.3.0February 26, 2026

Features

  • Presigned URL uploads — File uploads now bypass the API server entirely. The browser gets a presigned PUT URL, uploads directly to S3/R2/MinIO, then records the upload in the database. This fixes file uploads breaking behind reverse proxies (Dokploy/Traefik/Nginx) due to request body size limits and timeouts. Includes progress tracking via XHR.
  • Error pages for scaffolded apps — New grit new projects now include error.tsx, not-found.tsx, and global-error.tsx for both admin and web apps. Errors are displayed with styled UI instead of the default Next.js error page.
  • Production-ready Docker configdocker-compose.prod.yml now uses expose instead of ports, env_file for secrets, MinIO service, named bridge network, build args for NEXT_PUBLIC_API_URL, and Go 1.24.
  • Sentinel ExcludePaths — Pulse, GORM Studio, Sentinel, and API docs paths are now excluded from rate limiting by default, fixing Pulse health checks triggering rate limits.

Documentation

  • New Create without Docker guide — set up a Grit project using Neon, Upstash, Cloudflare R2, and Resend instead of Docker.

Infrastructure

  • Scaffold Dockerfile updated from Go 1.23 to Go 1.24
  • Next.js Dockerfile now accepts NEXT_PUBLIC_API_URL as a build argument
  • .env template includes Docker Compose production variables (POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB, API_URL)
v1.1.0February 25, 2026

Features

  • Default font changed to Onest — New projects scaffolded with grit new now use the Onest Google Font for all UI text instead of DM Sans. JetBrains Mono remains the code font. The font is loaded via next/font/google with weights 400, 500, 600, and 700.
  • Hire Us page — New /hire page for professional Grit development services. Includes service offerings, tech stack overview, and contact CTA.
  • Monetization banners — Docs sidebar now shows promotional cards for GritCMS, developer hiring services, and donations — visible on every documentation page.
  • Grit Fullstack Course page — New /course page with a 10-module curriculum covering Go, React, Next.js, and the full Grit stack.

Improvements

  • Top navigation now includes GritCMS, Hire Us, and a Sponsor heart icon for quick access to all revenue channels.
  • richtext added to the FieldType union for better type safety in the code generator.

Bug Fixes

  • OAuth callback fix — Fixed TokenPair struct field access in the social login callback handler (was using map indexing instead of struct fields).
  • Course waitlist fix — Fixed Google Sheets submission to use form-encoded data instead of JSON.

Documentation

  • New CLI Cheatsheet page — complete reference for all Grit CLI commands with flags, field types, generated files, common workflows, and full command tree.
  • New Social Login (OAuth2) setup guide for Google and GitHub authentication.
  • Updated Docker Cheat Sheet with force remove commands for containers and volumes.
  • Updated AI skill guide with social login (OAuth2) section.
v1.0.0February 24, 2026

Features

  • Social Login (Google + GitHub) — Every grit new project now includes OAuth2 social authentication via Gothic. Users can sign in with Google or GitHub on all auth pages (login, register, admin). Accounts are linked by email — existing users who sign in with a social provider are automatically connected. Configurable via GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID environment variables.
  • GORM Studio v1.0.1 — Updated to the first stable tagged release of GORM Studio.

Improvements

  • User model now includes Provider, GoogleID, and GithubID fields for social account linking. Password field is now nullable to support OAuth-only accounts.
  • Admin users table shows Provider column with badges (Email, Google, GitHub) and new filter option.
  • Social login buttons (Google + GitHub) appear on all 4 admin style variants (default, modern, minimal, glass).
v0.19.0February 24, 2026

Fixes

  • gin-docs AuthConfig — Updated scaffold template to use the new gindocs.AuthConfig struct instead of the deprecated gindocs.AuthBearer constant, fixing compilation errors in newly scaffolded projects.

Documentation

  • New Your First App tutorial — step-by-step Contact Manager guide covering project setup, resource generation, and CRUD
  • New Dokploy Deployment guide with Dockerfile examples
  • Improved terminal blocks across all tutorials with copy buttons and horizontal scroll
  • Updated API Documentation page to reflect the new AuthConfig struct format
v0.18.0February 22, 2026

Features

  • Pulse (Observability) — Every grit new project now includes Pulse, a self-hosted observability SDK. Provides request tracing, database monitoring, runtime metrics, error tracking, health checks, alerting, Prometheus export, and an embedded React dashboard at /pulse. Enabled by default, configurable via PULSE_ENABLED. See Pulse docs.

Documentation

  • New Pulse (Observability) page covering configuration, endpoints, health checks, alerting, Prometheus metrics, and data storage
v0.17.0February 22, 2026

Features

  • API Documentation (gin-docs) — Replaced hand-written Scalar/OpenAPI spec with gin-docs, a zero-annotation API documentation generator. Routes and GORM models are introspected automatically to produce an OpenAPI 3.1 spec with interactive Scalar or Swagger UI, plus Postman and Insomnia export.
  • Dark/Light mode for Go Playground — The playground now follows the site-wide theme toggle, switching between VS Code dark and light CodeMirror themes.
  • Umami Analytics — Optional visitor analytics via self-hosted Umami, configured with NEXT_PUBLIC_UMAMI_WEBSITE_ID environment variable.

Documentation

  • New API Documentation page covering gin-docs configuration, GORM model schemas, route customization, UI switching, and spec export
  • Full SEO + AEO implementation: sitemap, robots.txt, JSON-LD structured data, per-page metadata

Infrastructure

  • Added Dockerfile for docs site deployment (Next.js standalone output)
  • Google Search Console verification
v0.16.0February 21, 2026

Features

  • Go Playground — Interactive code editor at /playground with Go syntax highlighting, code execution via the official Go Playground API, example snippets, share links, and keyboard shortcuts (Ctrl+Enter to run).
  • GORM Studio updated — Updated to latest version with raw SQL editor, schema export (SQL/JSON/YAML/DBML/ERD), data import/export (JSON/CSV/SQL/XLSX), and Go model generation from database schema.

Documentation

  • Go for Grit Developers — comprehensive rewrite with 22 sections covering methods, Gin routing, middleware, CORS, handler/service architecture, GORM CRUD, migrations, seeding, JWT auth flow, and RBAC
  • Fixed right-side table of contents for the Go prerequisites page
  • New Middleware and CORS sections added to Go guide
v0.15.0February 20, 2026

Features

  • Security (Sentinel) — Every grit new project now ships with a production-grade security suite powered by Sentinel. Includes WAF, rate limiting, brute-force protection, anomaly detection, IP geolocation, security headers, and a real-time threat dashboard at /sentinel/ui. See Security docs.
  • Admin security page — New System → Security page in the admin panel embeds the Sentinel dashboard for monitoring threats without leaving the admin UI.

Documentation

  • New: Security (Sentinel) documentation page
  • Migrated getting-started pages (Installation, Quick Start, Troubleshooting) to use CodeBlock component
  • Added prerequisite learning pages for Go, Next.js, and Docker
v0.14.0February 18, 2026

Features

  • Multi-step forms — New formView: "modal-steps" and "page-steps" variants with horizontal/vertical step indicators, per-step validation, progress bar, and clickable step navigation. See Multi-Step Forms.
  • Standalone component usage — FormBuilder, FormStepper, and DataTable can now be used on any page in both web and admin apps without the resource system. See Standalone Usage.
  • Richtext field type — New richtext field with Tiptap WYSIWYG editor (bold, italic, headings, lists, code blocks, links, undo/redo).
  • string_array field type — Store arrays of strings using datatypes.JSONSlice[string]. Works with PostgreSQL and SQLite. Maps to string[] in TypeScript and z.array(z.string()) in Zod.
  • Built-in blog examplegrit new now scaffolds a complete blog with model, service, handler, seed data, public web pages, and admin resource definition.
  • Sidebar user avatar — Admin sidebar shows the current user's avatar with a dropdown menu for profile and logout.
  • Profile avatar upload — Profile page now supports avatar image upload.
  • react-hook-form in web app — Web app scaffold now includes react-hook-form as a dependency, enabling standalone FormBuilder usage out of the box.

Bug Fixes

  • Scalar API docs crash — Fixed c.String treating HTML as a format string. Now uses c.Data to avoid panics when Scalar HTML contains % characters in CSS/JS.
  • Blog route conflict — Admin blog CRUD routes moved from /api/blogs to /api/admin/blogs to avoid conflict with public blog routes.
  • Select dropdown styling — Fixed relationship select dropdown rendering behind modals using portal-based positioning.

Documentation

  • New: Build a Product Catalog tutorial — resource generation, multi-step forms, standalone DataTable & FormBuilder
  • New: Multi-Step Forms guide
  • New: Standalone Usage guide
  • New: Changelog page
  • Updated CLI Commands, Code Generation, Quick Start, Resources, Shared Package, Web App, Seeders, and Forms pages
v0.12.0February 2026

Features

  • Relationship support — New belongs_to and many_to_many field types for the code generator. Automatically creates foreign keys, junction tables, and relationship-aware form fields.
  • Relationship select fields — New relationship-select and multi-relationship-select form field components with search, portal-based dropdowns, and tag-based multi-select.
  • Beginner tutorial — "Learn Grit Step by Step" tutorial walking through building a full-stack app from scratch.
v0.11.0February 2026

Features

  • Full-page form view — New formView: "page" option renders forms as dedicated pages instead of modals.
  • slug field type — Auto-generates URL-friendly slugs with unique suffixes. Excluded from create/update forms and Zod schemas.
  • DataTable column customization — Hide/show columns, column visibility toggle in table toolbar.
  • grit start commandsgrit start client and grit start server for running frontend and API separately.
v0.10.0January 2026

Features

  • Style variants--style flag for grit new with 4 admin panel styles: default, modern, minimal, and glass.
  • Air hot reloading — Go API development with automatic rebuild on file changes using Air.
  • grit remove resource — Remove a generated resource and clean up all injected code (model, handler, routes, schemas, types, hooks, admin pages).
  • AI workflow docs — Guides for using Grit with Claude and Antigravity AI assistants.