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.
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 UPDATElock 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 newAuditDroppedCount()helper for monitoring saturation. audit.VerifyChain— was loading the entireactivity_logtable 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), honourscontextcancellation, 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 underRLockthen 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).
responseCaptureswitches[]byteappend tobytes.Buffer— 3 allocations instead of one per Write chunk. - Generated service queries —
Updatedropped the redundant thirdFirst()afterUpdates()(Updates mutates the loaded struct in place);Deletedropped the preflightFirst()(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 usesFindInBatchesin 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. Newexport.CSVRows()helper for header-less subsequent batches.
Medium fixes
- Webhook Replay —
retry_countincrement is now atomic viagorm.Expr("retry_count + ?", 1). Two concurrent replays of the same event no longer race to write the same +1 result. - Flags
bucketForfor anonymous users —crypto/rand.Readinstead oftime.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() % Nfor randomness — biased by call frequency; usecrypto/rand.
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.RulesJSON holdsrollout_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.Variantsis 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
RulesJSON also returnsfalse— 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.
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
- Route hits → look up provider (404 if unknown).
- Read raw body + headers.
provider.Verify(secret, body, headers)— 401 on signature mismatch.provider.Extract(body, headers)returns(eventType, externalID).- Insert into
webhook_events— UNIQUE on(provider, external_id)means duplicate deliveries becomestatus=skippedno-ops. webhooks.Dispatch(ctx, event)runs the registered handler for(provider, eventType), falling back to a catch-all""handler if no specific match.- Handler success →
status=processed; handler error →status=failed+handler_errorrecorded. Provider always gets200once 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'st=...,v1=...scheme with 5-minute replay tolerance.GitHubVerifier— GitHub'sX-Hub-Signature-256: sha256=...header.- Roll your own
VerifyFuncfor anything else — it's justfunc(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— readsX-GitHub-Event+X-GitHub-Deliveryheaders.
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. Incrementsretry_countand 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).
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'sHash. - 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 UPDATElock 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 overprevHash || 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. ReturnsChainStatuswith 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/DELETEonactivity_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.
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 toc.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.
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.Listgains opt-in cursor mode viaConfig.CursorMode: true. Response carriesMeta.NextCursor+Meta.HasMoreinstead ofPage/Pages.- Detects
HasMoreby fetchingPageSize + 1rows — 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 extraCOUNT(*), 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+ namedurl/image/avatar/thumbnail/logo/cover/icon/banner/photo) getsize:500instead ofsize: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/messagegettype:text. - Money fields on
floattype (suffix_amount/_price/_total/_cost/_fee/_balance/_rent/_salary/_wage/_value/_revenue/_deposit+ namedamount/price/total/cost/fee/balance/subtotal) gettype:decimal(12,2)for fixed-precision storage. No more1.99 + 0.01 = 1.9999999.
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/exportpackage:CSV(w, items, opts)andXLSX(w, items, opts)with a typedColumn{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 tofmt.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 injectGET /api/<plural>/exportautomatically. - Honours the same
searchparam as List, so users can export a filtered subset. - Adds
github.com/xuri/excelize/v2 v2.8.1to scaffoldedgo.mod.
Activity audit log — #32
- New
models.ActivityLogwith user_id + method + path + status + payload digest (sha256, not raw body) + IP + user-agent + duration. UUID PK;created_atindexed 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) withpaginate.Listfiltering byuser_id,method, andpathprefix. 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 axioserr.messageplus a fallback sotoast.error(apiErrorMessage(err))is always meaningful. apiErrorCodereturns the envelope'scodestring ("VALIDATION_ERROR","VERSION_CONFLICT", etc.) for branching logic.apiErrorFieldssurfaces per-field validation details so forms can highlight specific inputs.- New
internal/respondpackage on the server side too:respond.NotFound / Validation / Forbidden / Conflict / Internalfor handlers, replacing ad-hoc inlinec.JSON(500, gin.H{...}).
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.tsxrewritten 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 dashboardOutletwith this.
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 intcolumn added to User, Upload, Blog. ABeforeUpdateGORM hook auto-increments on every server-side write. The resource generator emits both on every new model.POST /api/sync/pushaccepts a batch of changes; each entry includes the version the client believes the server has. On mismatch the response containsVERSION_CONFLICT+ the current server state, so the client can drive a merge UI.GET /api/sync/pull?model=X&since=cursorreturns every row in the table updated after the cursor, paginated, with a new cursor in the response.- New
internal/sync/registry.gomaps logical table names (e.g."buildings") toreflect.Typeso the handler decodes dynamic payloads. New resources auto-register via// grit:syncmarker.
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 nowbutton 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.
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 withCounter(the GORM-backed row),Config(name + prefix + reset + width), and an atomicNext(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 theModels()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 UPDATEon 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 neverFlags:
--prefix— alphabetic prefix (default: first 3 chars of the name, uppercased)--reset— when the counter resets:monthly(default),yearly, ornever--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.
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. OneHubper process; each user can have multiple connections (desktop + mobile + web). Hub.SendToUser(userID, evt),SendToUsers(ids, evt), andBroadcast(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.goupgrades 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 ownresource.<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
realtimeBusEventTarget — any component can subscribe. - Start from
AuthProviderafter tokens land, stop on logout. - New hook:
useRealtimeEvent<T>(type, callback)subscribes to a typed topic and unsubscribes on unmount. PlususeRealtimeAny()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
Blogmodel inapi_blog_files.goswaps fromgorm.Model(auto-incr uint) to a string UUID PK with aBeforeCreatehook. Service signatures (GetByID,Update,Delete) and handler param parsing all switch fromuinttostring. - Standalone desktop scaffold (
grit new-desktop): User, Blog, and Contact models all switch to string UUID PKs withBeforeCreatehooks. All Wails-bound App methods and underlying service signatures useid string. Frontend mutation typings follow. - Shared TS types:
User,Upload, andBloginterfaces all useid: string. URL builders inAPI_ROUTEStakeid: string. TheBlogSchemaZod schema usesz.string(). - Admin TS:
DataTableselection state, genericuseResourceItem/useUpdateResource/useDeleteResourcemutation typings,RelationshipSelectFieldsingle value,MultiRelationshipSelectFieldarray values, andhandleDeletecallbacks all switch fromnumbertostring.
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.
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=truerenders anEmptyStatewith the configured title/hint instead.EmptyState,DetailSection(small caps section header), andDetailField(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 intoreact-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 withisPendingsupport (button disables, label flips to "Saving...").
components/filter-chip.tsx — filter chips
FilterChip— toggleable pill, active state shows accent background; optionalonClearrenders an X to clear a single filter; optionalcountrenders a small count badge.FilterBar— horizontal scrollable wrapper. Drop intoListPane'sfiltersslot.
Tailwind tokens
- Added
listpanespacing token (22rem/ 352px) to the desktop Tailwind config sow-listpaneworks.
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 intoroutes.Setupas a global middleware. - Activates only when the request carries an
Idempotency-Keyheader and the method isPOST/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: trueresponse 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-Keyon 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()atfrontend/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.
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.DBquery.paginate.Bind(c)readspage,page_size,search,sort_by,sort_orderfrom the Gin query, clampspageto ≥ 1 andpage_sizeto[1, 100].paginate.Configwhitelists sortable columns and declares the searchable column set — requests for columns outside the whitelist fall back tocreated_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 inILIKEsearch — a leftover rough edge from issue #12.
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"toconfig.go,"gorm.io/gorm/logger"touser.go,"net/http"tomiddleware/logger.go. - Stray package prefix: removed
handlers.qualifier onIsTrustedDevice(same-package call). - User ID type consistency: normalized
UserIDandUploadIDtostringUUIDs across 2FA models, auth service, TOTP handler (c.GetString("user_id")replacesc.GetUint), jobs package, and upload handler.
Desktop scaffold fixes
keychain.gomoved frominternal/to the top level (the subdirectory file was declaringpackage main, which Go rejects).go.modmodule path fixed from<project>/apps/api/apps/desktopto<project>/apps/desktop.
Resource generator fixes
- Service signatures take
id stringinstead ofid 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(wereuint/number). - Initialism-aware
toPascalCase/toSnakeCase:owner_id→OwnerID(wasOwnerId),image_url→ImageURL(wasImageUrl),api_key→APIKey. 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 and
ShouldBindJSONmismatches). - Zod FK and M2M validators use
z.string().uuid()instead ofz.number().int(). - FK columns generate with
gorm:"size:36;index"(matches UUID PK width).
New: --desktop flag
- Desktop + mobile + API in one monorepo —
grit new myapp --mobile --desktopscaffolds a complete multi-client SaaS: Go API shared by an Expo mobile app AND a Wails desktop app. All three share the samepackages/sharedtypes 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.--desktopis 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 via
GetPlatform()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:⌘Kpalette,⌘,settings,⌘Llogout,Escto close. - More negative space — content padding is
32px(vs web's24px) for long focus sessions. Subtler shadows (OS chrome already provides elevation).
Style Guide
- New §14.5 Desktop App Patterns section in
GRIT_STYLE_GUIDE.mdcovering 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)Design System
- GRIT_STYLE_GUIDE.md — First official style guide for all Grit-scaffolded projects. Premium Minimal aesthetic (Linear / Vercel school), Grit purple
#6C5CE7primary, 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 atcomponents/layout/page-header.tsxwith 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 withstats: false.
Auth Pages
- New centered auth variant —
grit new myapp --style centeredscaffolds 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).
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_SECRETis shorter than 32 characters. - Sentinel WAF — Now runs in
ModeBlockin production (was alwaysModeLog). Development keepsModeLog.
Performance
- GORM AutoMigrate silence — Migration now uses a
logger.Silentsession 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 withAuthProvidercontext 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-browserwith 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.
Features
- Scaffold into current directory —
grit new .andgrit 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. --forceflag — Allows scaffolding into non-empty directories. Useful when a repo was cloned first (with README, .git, LICENSE) before scaffolding:grit new . --triple --vite --force.--hereflag — Explicit alternative togrit 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 --viteno longer shows the architecture/frontend selection prompt. Flags act as true shortcuts for non-interactive setup. - Module path upgrade to /v3 — Fixed
go install ...@latestdownloading v2.9.0 instead of v3.x. All import paths updated fromgrit/v2togrit/v3.
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 newStepWithCodecomponent 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), andgrit 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.
Features
- Multi-architecture code generator —
grit 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.jsonproject manifest — Every scaffolded project now includes agrit.jsonfile at the root witharchitectureandfrontendfields. 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 at
src/routes/_dashboard/resources/usingcreateFileRouteinstead of Next.jsapp/(dashboard)/resources/page convention.
Features (Goravel-Inspired)
grit routes— List all registered API routes in a formatted table. Parsesroutes.goand 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 downcreates a.maintenancefile that triggers the new maintenance middleware, returning 503 for all requests.grit upremoves 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,--keyflags orDEPLOY_HOST/DEPLOY_DOMAIN/DEPLOY_KEY_FILEenv vars.- Maintenance middleware — All scaffolded projects now include a
Maintenance()Gin middleware that checks for a.maintenancefile on every request. Runs as the first global middleware.
Features
- Single app architecture —
grit new my-app --singlecreates a single Go binary that serves both the API and an embedded React SPA. Usesgo:embedto bake the built frontend into the binary at compile time. One file to deploy. Dev mode runs Go on:8080and Vite on:5173with API proxy. - Parameterized API paths — All Go API file generators now use
opts.APIRoot()andopts.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 withgo:embed frontend/dist/*and SPA fallback routinginternal/— Full Go backend (same as monorepo API)frontend/— React + Vite + TanStack Router SPAMakefile—make dev(parallel servers),make build(single binary)
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.
Features
- Interactive project creation —
grit new my-appnow 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
Optionsstruct now usesArchitectureandFrontendenum fields instead of boolean flags. Legacy flags (--api,--mobile,--full) still work via theNormalize()migration layer.
Features
- Two-Factor Authentication (TOTP) — Every
grit newproject 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 2FAPOST /api/auth/totp/verify— Verify TOTP code during login (public, uses pending token)POST /api/auth/totp/backup-codes/verify— Use backup code during loginPOST /api/auth/totp/disable— Disable 2FA (requires password)GET /api/auth/totp/status— Check 2FA status, remaining backup codes, trusted device countPOST /api/auth/totp/backup-codes— Regenerate backup codesDELETE /api/auth/totp/trusted-devices— Revoke all trusted devices
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/modelformat (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 variables —
AI_PROVIDER,AI_API_KEY, andAI_MODELhave been replaced withAI_GATEWAY_API_KEY,AI_GATEWAY_MODEL, andAI_GATEWAY_URL. Update your.envfile accordingly. Get your API key from vercel.com/ai-gateway.
Features
- 10 Official Plugins — New
grit-pluginsecosystem 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.mdto 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.
Fixes
- GORM Studio (Desktop) — Replaced the broken custom HTML studio with the real
gorm-studiopackage. Desktop studio now runs on port 8080 at/studiousing Gin + gorm-studio, matching the web scaffold. Auto-opens browser on launch.
Features
- GRIT_SKILL.md — Desktop scaffolds now include a
GRIT_SKILL.mdfile 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.mdnow 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.
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
tscfrom the frontend build script. TanStack Router's Vite plugin generatesrouteTree.gen.tsduring the Vite build, so runningtscbefore Vite causedCannot find module './routeTree.gen'errors. - Title bar import path — Fixed the Wails binding import in
title-bar.tsxfrom a 2-level to 3-level relative path. - Auth hook file extension — Renamed
use-auth.tstouse-auth.tsxso 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.
Fixes
- Desktop auth hook file extension — Renamed the scaffolded
use-auth.tstouse-auth.tsxso TypeScript correctly handles the JSX in<AuthContext.Provider>. Previously,grit new-desktopprojects would fail to compile withTS1005: '>' expectederrors.
Documentation
- Added Desktop Handbook PDF download links to all 8 desktop documentation pages.
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 andRoute.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
lgbreakpoint. Opens a Sheet sidebar with all navigation links. Auto-closes on link click. - CGO-free SQLite — Replaced
gorm.io/driver/sqlite(requires CGO) withgithub.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 generatecommands.
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.
Features
- Native desktop apps (Wails) — New
grit new-desktopcommand 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 generation —
grit generate resourcenow 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) usinggrit: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 startfor desktop — Runninggrit startinside a desktop project launcheswails devwith hot-reload for both Go and React.grit compile— New command that runswails buildto 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 resourcefor 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 addfrom/rendpoints.
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.
Features
- Gzip response compression — All API responses are now compressed automatically via a custom
Gzip()middleware using the Go standard librarycompress/gzipatBestSpeed. JSON payloads shrink by 60–80%, reducing bandwidth on paginated list endpoints with zero external dependencies. - Request ID tracing — A
RequestID()middleware injects a uniqueX-Request-IDheader 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), andConnMaxIdleTime(10m). This prevents stale connections after network interruptions and avoids connection exhaustion under load. - Cache-Control headers on public blog endpoints — The
ListPublishedhandler now returnsCache-Control: public, max-age=300(5 minutes) andGetBySlugreturnsCache-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.
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 newprojects now includeerror.tsx,not-found.tsx, andglobal-error.tsxfor both admin and web apps. Errors are displayed with styled UI instead of the default Next.js error page. - Production-ready Docker config —
docker-compose.prod.ymlnow usesexposeinstead ofports,env_filefor secrets, MinIO service, named bridge network, build args forNEXT_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_URLas a build argument .envtemplate includes Docker Compose production variables (POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB,API_URL)
Features
- Default font changed to Onest — New projects scaffolded with
grit newnow use the Onest Google Font for all UI text instead of DM Sans. JetBrains Mono remains the code font. The font is loaded vianext/font/googlewith 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.
richtextadded to the FieldType union for better type safety in the code generator.
Bug Fixes
- OAuth callback fix — Fixed
TokenPairstruct 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.
Features
- Social Login (Google + GitHub) — Every
grit newproject 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 viaGOOGLE_CLIENT_ID,GITHUB_CLIENT_IDenvironment variables. - GORM Studio v1.0.1 — Updated to the first stable tagged release of GORM Studio.
Improvements
- User model now includes
Provider,GoogleID, andGithubIDfields 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).
Fixes
- gin-docs AuthConfig — Updated scaffold template to use the new
gindocs.AuthConfigstruct instead of the deprecatedgindocs.AuthBearerconstant, 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
AuthConfigstruct format
Features
- Pulse (Observability) — Every
grit newproject 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 viaPULSE_ENABLED. See Pulse docs.
Documentation
- New Pulse (Observability) page covering configuration, endpoints, health checks, alerting, Prometheus metrics, and data storage
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_IDenvironment 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
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
Features
- Security (Sentinel) — Every
grit newproject 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
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
richtextfield with Tiptap WYSIWYG editor (bold, italic, headings, lists, code blocks, links, undo/redo). string_arrayfield type — Store arrays of strings usingdatatypes.JSONSlice[string]. Works with PostgreSQL and SQLite. Maps tostring[]in TypeScript andz.array(z.string())in Zod.- Built-in blog example —
grit newnow 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-formin web app — Web app scaffold now includesreact-hook-formas a dependency, enabling standalone FormBuilder usage out of the box.
Bug Fixes
- Scalar API docs crash — Fixed
c.Stringtreating HTML as a format string. Now usesc.Datato avoid panics when Scalar HTML contains%characters in CSS/JS. - Blog route conflict — Admin blog CRUD routes moved from
/api/blogsto/api/admin/blogsto 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
Features
- Relationship support — New
belongs_toandmany_to_manyfield types for the code generator. Automatically creates foreign keys, junction tables, and relationship-aware form fields. - Relationship select fields — New
relationship-selectandmulti-relationship-selectform 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.
Features
- Full-page form view — New
formView: "page"option renders forms as dedicated pages instead of modals. slugfield 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 startcommands —grit start clientandgrit start serverfor running frontend and API separately.
Features
- Style variants —
--styleflag forgrit newwith 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.