grit · git:20261005.aab803d · 2026-10-05 · sha256 e9df5aac8cce857f
grit git:20261005.aab803dA
Immutable. This exact content never changes and is served at /api/v1/blob/e9df5aac8cce857f.
---
name: grit
description: >
Grit framework conventions and patterns for this triple project.
Use when modifying models, handlers, routes, schemas, types, or components.
Automatically loaded as background knowledge.
user-invocable: false
---
# Grit Framework — Triple (Web + Admin + API) + Next.js
This project uses a **triple** architecture: Go API + React web frontend + admin panel in a Turborepo monorepo. Three apps that share types and schemas.
It uses **Next.js** (App Router) for the frontend — SSR, SSG, ISR, SEO-optimized.
**Batteries included:** file storage (S3), email (Resend), background jobs (asynq), cron, Redis cache, AI (Vercel AI Gateway), security (Sentinel), observability (Pulse), auto-generated API docs (gin-docs).
For detailed API conventions, code patterns, and service documentation, see [reference.md](reference.md).
---
## CLI Commands
`ash
# Code generation
grit generate resource Post --fields "title:string,content:text,published:bool"
grit generate resource Post --from post.yaml
grit remove resource Post # Cleanly removes all generated files + injections
# Development
grit start # Start dev servers
grit sync # Go types → TypeScript
grit add role MODERATOR # Injects role into 7 locations
grit migrate # Run GORM AutoMigrate
grit seed # Create admin + demo users
# Operations
grit routes # List all API routes
grit down # Enable maintenance mode (503)
grit up # Disable maintenance mode
grit deploy --host user@server --domain myapp.com # Production deploy
# Updates
grit upgrade # Update project to latest templates
grit update # Update Grit CLI itself
`
---
## Project Structure
`
commerce/
├── packages/shared/ # Zod schemas, TS types, constants
├── apps/
│ ├── api/ # Go backend (Gin + GORM)
│ │ ├── cmd/server/main.go
│ │ └── internal/ # config, database, models, handlers, services, middleware, routes
│ ├── web/ # Next.js frontend (App Router)
│ └── admin/ # Next.js admin panel (App Router)
├── .env
├── docker-compose.yml
└── turbo.json
`
**Mounted dashboards** (auto-configured in routes.go):
- `/docs` — API documentation (gin-docs, OpenAPI 3.1)
- `/studio` — Database browser (GORM Studio)
- `/sentinel/ui` — Security dashboard (WAF, rate limiting)
- `/pulse` — Observability (tracing, metrics)
---
## Generating Resources
`ash
grit generate resource Post --fields "title:string,content:text,published:bool,views:int"
`
Creates model, service, handler, schema, types, hooks, and injects into existing files via marker comments.
### Field Types
| Type | Go | TypeScript | Form |
|------|----|-----------|------|
| `string` | `string` | `string` | Text input |
| `text` | `string` | `string` | Textarea |
| `int` / `uint` / `float` | `int` / `uint` / `float64` | `number` | Number input |
| `bool` | `bool` | `boolean` | Toggle |
| `datetime` / `date` | `*time.Time` | `string | null` | Picker |
| `richtext` | `string` | `string` | Tiptap editor |
| `slug` | `string` | `string` | Auto-generated |
| `string_array` | `JSONSlice[string]` | `string[]` | Tag input |
| `email` / `url` / `domain` | `string` | `string` | Email, web address, bare domain |
| `tel` (`phone:tel:UG`) | `string` (E.164) | `string` | Country picker + as-you-type number |
| `country` | `string` (ISO alpha-2) | `string` | Searchable country picker |
| `color` / `time` | `string` | `string` | Colour picker, time input |
| `percent` / `rating` (`score:rating:10`) | `float64` / `int` | `number` | Number with %, stars |
| `json` | `datatypes.JSON` | `unknown` | JSON editor |
| `belongs_to:X` | `uint` (FK) | `number` | Relationship select |
| `many_to_many:X` | Junction table | `number[]` | Multi-select |
**Modifiers:** `:unique`, `:required`, `:optional` (append after type).
---
## Marker Comments
Grit uses marker comments to inject generated code. **Never delete these:**
`go
// grit:models — models/user.go (AutoMigrate list)
// grit:handlers — routes/routes.go (handler initialization)
// grit:routes:protected — routes/routes.go (protected route group)
// grit:routes:admin — routes/routes.go (admin route group)
`
`typescript
// grit:schemas — schemas/index.ts
// grit:types — types/index.ts
// grit:api-routes — constants/index.ts
`
`typescript
// grit:resources — resources/index.ts (imports)
// grit:resource-list — resources/index.ts (registry array)
`
---
## Frontend Routing
Routes live in app/. File naming: page.tsx (route), layout.tsx (layout), loading.tsx, error.tsx.
Use 'use client' directive for client components. Server Components by default.
Navigation: import { useRouter } from 'next/navigation', import Link from 'next/link'.
---
## Common Tasks
### Add a field to an existing resource
1. Add field to Go model (apps/api/internal/models/<name>.go)
2. Update handler if field needs special handling
3. Update Zod schema (packages/shared/schemas/<name>.ts)
4. Update TypeScript type (packages/shared/types/<name>.ts)
5. Update admin resource (apps/admin/resources/<name>.ts) — add column + form field
6. Restart API (GORM auto-migrates)
### Add a new API endpoint
1. Create/update handler in the handlers directory
2. Register route in routes.go
3. Create React Query hook if frontend needs it
### Add a relationship
`go
type Post struct {
CategoryID uint // json:"category_id"
Category Category // gorm:"foreignKey:CategoryID" json:"category,omitempty"
}
// In handler: query.Preload("Category").Find(&posts)
`
---
## Performance & Production Hygiene
Grit ships with audited primitives — but app code can still introduce
CPU burn / memory leaks / lock contention. Follow these rules so the
patterns the framework hands you stay efficient at production scale.
### Hot-path rules (request handlers + middleware)
- **Never spawn unbounded goroutines per-request.** If you need
background work, push to a buffered channel + a fixed-size worker
pool. Pattern shipped: `ActivityLogger` in
`internal/middleware/activity.go` — single writer, bounded
channel, drop on overflow.
- **Never hold a mutex across slow operations.** Read shared state
under the lock, copy what you need, release, then do the work.
Pattern shipped: `flags.Engine.evaluate` — copies the flag
struct under RLock, runs all rules unlocked.
- **Never load a whole table into memory.** Use
`paginate.List` for list endpoints,
`FindInBatches` for exports, `sync.VerifyChain`
pattern (cursor on (created_at, id)) for verification scans.
- **Never call SHA-256 in a hot path** unless cryptographic. Cache
keys, ID buckets, and hash maps should use `fnv.New64a` or
`xxhash` — a 50x speedup with no correctness loss. Pattern
shipped: cache middleware uses fnv.
- **Never write to the same row from multiple goroutines** without a
serialization point. The audit hash chain serializes by design via
the single-writer worker. If you need ad-hoc chained writes, use
`audit.AppendChained` (it takes the FOR UPDATE lock).
- **Never use `time.Now().UnixNano() % N` for randomness** — it's
biased toward the call frequency. Use `crypto/rand` or
`math/rand`. Pattern shipped: `flags.bucketFor` for
anonymous users uses crypto/rand.
### Database rules
- **N+1 queries**: always `Preload("Association")` when you'll
access an association inside a loop. The resource generator emits
Preloads for declared `belongs_to` fields automatically.
- **No SELECT * on large tables**: use `.Select("id", "name")`
when you only need a few columns.
- **No queries inside loops**: batch with `IN(?)` or
`CreateInBatches`. The webhook receiver and sync push handler
do batch inserts; mirror that pattern.
- **Indexes**: every column in a `WHERE` or `ORDER BY`
on a hot endpoint needs an index. Use the
`gorm:"index"` tag on the model.
- **Keep transactions short**: never hold a transaction open across
network calls (S3 upload, webhook callback, AI completion). Take the
lock, flush, release, then do the slow work.
### Background job rules
- **Job idempotency**: every handler must be safe to re-run. Use the
job's own ID as a dedup key (the asynq scaffold does this). For
webhooks, the `(provider, external_id)` UNIQUE constraint
enforces it for free.
- **Job timeouts**: pass a `context.WithTimeout` through every
job handler. Asynq propagates it via `ctx.Done()` — the job
is killed if it runs over.
- **Distributed locks for cron**: if you run >1 instance of the API,
cron jobs need a lock or they double-fire. Use Redis `SETNX`
with a TTL or asynq's `UniqueOpt`.
- **Bounded retries**: asynq retries with exponential backoff by
default; cap `MaxRetry` explicitly per job type so a poison
message doesn't burn the queue forever.
- **Per-queue worker limits**: don't share one worker pool across
`default` + `critical` + `low` queues. Set
per-queue concurrency in `jobs.go` so a low-priority backlog
can't starve criticals.
### Logging rules
- **No `log.Printf` in tight loops.** Sample (e.g.
`if i%1000 == 0 { log… }`) or move to debug level.
- **Never log full request/response bodies** — they may contain PII,
secrets, or megabyte-scale uploads. The activity logger stores
SHA-256 of the body, not the body itself. Follow that pattern.
- **Use structured logging at info+** — easier to grep, alert on, and
ship to a log aggregator.
### Memory rules
- **Caches need a TTL**: never cache without expiration. Redis is the
default cache; `cache.Set(ctx, key, val, ttl)` requires a TTL.
- **Channels need a bound**: `make(chan T, N)` with a real N,
never `make(chan T)` for high-volume work.
- **Maps that only grow are leaks** — if you keep a per-user cache,
evict on a TTL or use an LRU library.
### What's already audited
The Grit framework's own primitives have been performance-reviewed
through v3.22 — the patterns below are safe to call at scale:
- `ActivityLogger` middleware: bounded channel + single writer
- `audit.VerifyChain`: chunked walk with context cancellation
- `paginate.List`: limit-bounded queries, opt-in cursor mode
- `flags.Engine`: in-memory cache, copy-then-release locking
- `webhook receiver`: fire-and-forget dispatch, atomic retry
- `sync engine`: bounded squashed outbox, batched push
If you suspect CPU burn in production, the framework's own primitives
are unlikely to be the cause — check application code first
(unbounded goroutines, missing Preloads, sync work in handlers).
---
## Critical Rules
1. **Never delete marker comments** (`// grit:*`)
2. **Follow the response format** — `{ data, message }` / `{ data, meta }` / `{ error: { code, message } }`
3. **Always handle errors in Go** — never ignore with `_`
4. **Keep the folder structure** — don't move files
5. **Use React Query** for all data fetching — no raw fetch
6. **Use Zod** for validation — shared between frontend and backend
7. **Use Tailwind + shadcn/ui** — no custom CSS files
8. **Use App Router** — never Pages Router
9. **Report bugs** — if you encounter a bug, open an issue at https://github.com/MUKE-coder/grit/issues with the error message and steps to reproduce