Stability and hardening
What is proven, what is young, and where the line runs between what Grit guarantees and what stays your job. Per subsystem, so you can make your own risk call on the part you are about to depend on instead of on the whole framework at once.
The rule for the table below: a claim in Grit guarantees names the test that would fail if it stopped being true. Where no such test exists, the claim sits in your responsibility instead, however right the code looks. Three of the worst entries in the changelog were docs asserting a guarantee the code did not provide, so the discipline is the point.
- stable long-lived, covered by tests and live checks, and no correctness bug found in it recently.
- beta works and is tested, and has had a real bug found in it recently enough that you should verify your own case.
- new shipped in the last few weeks. Integration bugs are still plausible.
Auth: JWT, sessions, refresh rotation
stable- Grit guarantees
- Every refresh token is backed by a session row, rotation detects replay, and a password change signs out every device.
- Proven by
- handlers/auth_test.go, services/session_test.go, services/password_reset_test.go (41 handler tests, 75 service tests ship in your project)
- Your responsibility
- Set JWT_SECRET to 32+ random bytes and keep it out of the repository. grit doctor fails if it is short, empty or a placeholder.
- What went wrong once
- Two logins in the same second once produced identical refresh tokens (fixed in v3.86.0: every token carries a jti).
Two-factor, passkeys, recovery codes
beta- Grit guarantees
- TOTP is RFC 6238 (HMAC-SHA1 by the spec), recovery codes are single-use, and passkey challenges are bound to a session.
- Proven by
- services/passkey_test.go, services/recovery_test.go, crypto/field_test.go
- Your responsibility
- Decide your own lockout and recovery policy, and test the flow with the authenticator app your users actually have.
RBAC: roles, permissions, the staff group
beta- Grit guarantees
- A custom role reaches only the endpoints its permissions name, a revoked permission stops working everywhere, and a route that names no permission stays ADMIN-only, so one that forgets fails closed.
- Proven by
- authz/grants_test.go, authz/permissions_test.go, handlers/role_test.go, handlers/user_role_sync_test.go, plus a live check that a staff account holding notes.delete still cannot delete another user’s row
- Your responsibility
- Name a permission on every route you add yourself. The generator does it for generated resources; a hand-written route is yours.
- What went wrong once
- A custom role could not reach a single admin endpoint until v3.220.0; a revoked permission kept working on other replicas until v3.218.0.
Generated CRUD: services, handlers, pagination
stable- Grit guarantees
- Handlers run no queries; the service owns every read and write, takes a context, and whitelists what a client may search, sort, filter and patch, because those names reach SQL.
- Proven by
- internal/generate tests in the CLI (492 test functions across 99 files), an import-versus-use check on every generated file, and 57 live checks against a running app on Postgres 15, 16 and 17
- Your responsibility
- Put your own queries in the service, not the handler, so a job and a route cannot disagree.
- What went wrong once
- Handlers ran all of their own queries until v3.224.0, and the service written beside them was never called.
Owned resources (--owned-by)
stable- Grit guarantees
- List, export, get, PDF, update, patch, delete and bulk are all scoped to the caller; somebody else’s row is 404 rather than 403; the owner comes from the session and never from the request body. ADMIN is exempt.
- Proven by
- Live checks cover every one of those paths, including the staff-permission case, plus generator tests that fail if a method loses its scoping. grit doctor reports the same thing in your project.
- Your responsibility
- Add --owned-by when rows belong to a user. A resource that merely references a user is not scoped, and grit doctor asks about it.
- What went wrong once
- The export, PDF, patch and bulk routes were unscoped once: a second ordinary account printed another user’s record and rewrote it.
Optimistic locking (If-Match)
stable- Grit guarantees
- A read returns the version as an ETag, a write against a stale version is 409 naming the current one, and of twenty simultaneous writes on one version exactly one lands.
- Proven by
- concurrency/concurrency_test.go in your project, generator tests, and a 20-way race in the live suite on all three Postgres versions
- Your responsibility
- Send If-Match from your own clients. Without it the last write still wins, by design.
- What went wrong once
- Added in v3.219.0, after two people saving the same record meant the second silently won.
Encryption at rest (:encrypted fields)
beta- Grit guarantees
- An encrypted column is ciphertext in the database and plaintext through the API, and it is kept out of search, sort and filter lists, where ciphertext would match nothing.
- Proven by
- crypto/field_test.go, crypto/map_update_test.go, and a live check that reads the column straight out of Postgres
- Your responsibility
- Set FIELD_ENCRYPTION_KEY. Without it the value is stored as it is, and nothing fails: grit doctor makes this an error, and it is the first thing it found in a real project.
- What went wrong once
- A plaintext leak was fixed in v3.212.0. Re-audit before storing real PII.
Multitenancy (plugin, tenant.Owned)
beta- Grit guarantees
- A tenant-owned model is scoped by the organization on the request context on every authenticated route group, a query with no organization is refused rather than answered (400 NO_ORGANIZATION), the X-Organization-ID header is checked against membership, and a role held through a membership grants inside that organization only.
- Proven by
- A CI job scaffolds a project with the multitenant and impersonate plugins and runs 26 checks across the seams on a real Postgres: cross-tenant reads, an admin with no organization, an organization and an owner on one row, a membership role used in the wrong organization, and an impersonated session inside a tenant. It also asserts the middleware order that one of those bugs was.
- Your responsibility
- Add tenant.Owned to every model that belongs to an organization. Decide which organization a user provisioned by SSO joins: nothing does it for you, and grit doctor says so. Combining tenancy with --tree or --public is still not covered by the fixture.
- What went wrong once
- Isolation bugs were fixed in v3.197 and v3.198. v3.233.0 fixed three found by building tenancy, roles and impersonation together: every DELETE on a tenant-owned row answered 500 because the staff group never resolved an organization, a membership role granted nothing at all, and “no active organization” was an opaque 500.
Money (exact, multi-currency)
stable- Grit guarantees
- Amounts are integer minor units with their currency, two money columns per row do not collide, and sums stay exact.
- Proven by
- money/money_test.go (13 tests), generator tests for the embedded columns and whitelists, and a live round-trip check
- Your responsibility
- Pick the currency per row deliberately; Grit does not convert between currencies.
CSV/XLSX import and export
beta- Grit guarantees
- An export streams every matching row; an import runs in the background with a job to poll, resolves relations, skips duplicates, and an owned resource’s rows belong to whoever imported them unless an ADMIN names another owner.
- Proven by
- Live checks for both, including the owned cases, and generator tests for the importer’s columns
- Your responsibility
- Validate the files your users upload. The parser is hardened and bounded, and it is still parsing a stranger’s spreadsheet.
- What went wrong once
- Every export was an empty 200 once. The importer created a user for every unrecognised email until that was removed.
Trees (--tree)
beta- Grit guarantees
- A move carries its subtree, a reorder keeps the parent the node has, and a node cannot be moved under its own descendant.
- Proven by
- services/category_tree_test.go ships with the resource (10 tests), plus live checks for the move, the reorder and the refusal
- Your responsibility
- Run the rebuild endpoint once after adding --tree to a table that already had rows.
- What went wrong once
- Dragging a node that predated --tree once added one to the depth of every row in the table.
Public API (--public)
beta- Grit guarantees
- Public reads are an allowlist struct rather than the model, they are guarded by an API key, archived rows are excluded, and cost-shaped columns are held back by default.
- Proven by
- Live checks for the key, the allowlist, the filters and the archived case; grit doctor reports a held-back column that was published by hand
- Your responsibility
- Review the allowlist when you add fields; it is your file and the generator will not rewrite it. Public responses are cached by URL for cache.public_ttl_seconds (60 by default), so an archived row can linger in a cached list for up to a minute.
Append-only resources
beta- Grit guarantees
- Rows are created and read, never changed or deleted: a GORM guard, no mutating routes, and a database trigger that refuses the write.
- Proven by
- appendonly/appendonly_test.go, and grit doctor reports an append-only resource that still mounts a write
- Your responsibility
- Run grit migrate so the trigger exists. The guard alone is not the database refusing.
Realtime (WebSockets, Redis backplane)
beta- Grit guarantees
- Events reach a user on every replica through the backplane, and a closed socket is cleaned up.
- Proven by
- realtime/backplane_test.go, cluster/cluster_test.go
- Your responsibility
- Run Redis if you run more than one replica. Without it, events stay in the process that emitted them.
- What went wrong once
- Cross-replica and revocation bugs were fixed in v3.193 and v3.196.
Durable events (outbox)
beta- Grit guarantees
- A durable subscriber’s work is committed with the row that triggered it, and the relay delivers it once.
- Proven by
- outbox/outbox_test.go (12 tests)
- Your responsibility
- Start the relay. grit upgrade wires it for projects that predate it.
Error responses
new- Grit guarantees
- Every error is the same envelope, each code is documented with what it means and what a client should do, and a code always arrives with the same status. The typed codes in Go and the union type in TypeScript are generated from one catalogue, so they cannot disagree.
- Proven by
- internal/errorcodes walks every template: a code that is not catalogued, or that is returned with a status other than its own, fails the build
- Your responsibility
- Codes your own handlers invent are yours to document. respond.Rule covers a business rule without adding one.
- What went wrong once
- Shipped in v3.232.0, which reconciled 31 sites. VALIDATION_ERROR was 422 in thirty-eight handlers and 400 in twenty-five, INVALID_TOKEN was both 401 and 400, and an upload that did not exist answered 400.
Migrations and rollback
new- Grit guarantees
- Every run records what it changed, taken from the schema before and after, and grit migrate down drops exactly that: indexes, then columns, then tables. A drop already done is skipped, so an interrupted rollback can be re-run. The run that built the schema is a baseline and is refused.
- Proven by
- internal/migrate tests ship in every project, and the live suite adds a column with grit generate field, rolls it back on a real Postgres and checks the column is gone and the table is not
- Your responsibility
- Read the statements before you answer the prompt. A rollback drops columns, and the data in them is not recoverable: this undoes a schema change, not a deployment.
- What went wrong once
- Shipped in v3.230.0. The first version counted SQLite’s own sqlite_sequence table as part of the schema, so on SQLite no run would have been recognised as the baseline and a rollback would have offered to drop everything.
Backups and restore
beta- Grit guarantees
- A backup streams row by row, and a restore writes parents before children.
- Proven by
- The CLI’s backup tests and the generated project’s backup command
- Your responsibility
- Restore into a scratch database and look at the rows. A backup nobody has restored is a hope.
- What went wrong once
- Restore correctness bugs were fixed in v3.207.0 and v3.213.0.
Offline sync (desktop)
new- Grit guarantees
- Registered models mirror to the client, and conflicts resolve by the policy you set.
- Proven by
- The sync engine’s tests in the CLI, and grit sync doctor reports a model that cannot work (no version column, an allowlist naming a column that does not exist)
- Your responsibility
- Run grit sync doctor, and test the flows your users will have offline.
Workflows (status state machines)
new- Grit guarantees
- Only declared transitions are allowed, they are guarded by role, and a transition hook commits with the move or not at all.
- Proven by
- The generated workflow service’s tests, plus generator tests for the transition route
- Your responsibility
- Decide who may make each transition. The generator gives you the shape, not your policy.
- What went wrong once
- Shipped in v3.221.0: before it, a workflow could change a status and nothing else.
Feature flags
new- Grit guarantees
- A flag can target attributes, a percentage and a date window, and flags.IsEnabled works from anywhere, answering false before the engine starts.
- Proven by
- flags/flags_test.go ships in every project and runs against a real engine
- Your responsibility
- Manage flags through the API: there is no admin screen for them yet.
- What went wrong once
- Shipped in v3.223.0, when the documented package-level API did not exist at all.
Desktop (Wails) and mobile (Expo)
new- Grit guarantees
- Both share the monorepo API and its generated types.
- Proven by
- The desktop module builds in CI; the Expo app is built by the nightly canary
- Your responsibility
- Signing and store submission. A signed desktop release has never been verified end to end, because that needs Authenticode and Apple Developer certificates this project does not own.
How Grit is tested
Nearly every serious bug in Grit's history was found the same way: by building a real application on it and using it until something was wrong. That method works, and it only protects the next release if what it found is kept as a test. So it is.
The detail is in how Grit itself is tested, and the project-side audit is grit doctor.
What is not covered yet
Stated plainly, because a matrix that only lists strengths is marketing:
- No independent security audit. Every bug in auth, tenancy and encryption listed above was found by the person who wrote it. An adversarial review looks for what should not be possible, which is a different search.
- The framework's own handlers still query directly. Auth, two-factor, uploads, form shares and the dashboard read and write from their handlers. Generated resources do not, since v3.225.0.
- Multitenancy is not covered by the live suite, and not at all in combination with
--treeor--public. - No LTS channel. Releases land daily on one track. If you need a slower train, pin a version and read the changelog before moving.
- No signed desktop release has been verified end to end.
- No production case studies yet. Benchmarks and tests are not the same evidence as somebody else's traffic.
