Architecture Overview
Grit is a full-stack monorepo that fuses a Go backend with Next.js frontends, a shared type package, and an admin panel. This page explains how all the layers fit together.
Monorepo Structure
Every Grit project is a Turborepo-powered monorepo managed by pnpm. The Go API lives alongside the Next.js frontends and a shared TypeScript package. This structure allows all apps to share validation schemas, types, and constants from a single source of truth.
myapp/ ├── apps/ │ ├── api/ # Go backend (Gin + GORM) │ ├── web/ # Next.js main frontend │ └── admin/ # Next.js admin panel ├── packages/ │ └── shared/ # Zod schemas, TS types, constants ├── docker-compose.yml # PostgreSQL, Redis, MinIO, Mailhog ├── turbo.json # Monorepo task orchestration ├── pnpm-workspace.yaml # Workspace definition └── .env # Environment variables
Turborepo handles parallel builds and caching across apps. The pnpm-workspace.yaml file links the frontend apps to the shared package, so importing @shared/schemas works without publishing to npm.
High-Level Architecture
The following diagram shows how the layers communicate. The browser talks to the Next.js frontends, which talk to the Go API over REST. The Go API manages the database, cache, file storage, job queue, and email.
┌─────────────────────────────────────────────────────────┐
│ BROWSER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web App │ │ Admin Panel │ │ GORM Studio │ │
│ │ :3000 │ │ :3001 │ │ :8080/studio│ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────────┼──────────────┼──────────────┼─────────────┘
│ │ │
REST + JWT REST + JWT Direct
│ │ │
┌─────────────┴──────────────┴──────────────┴─────────────┐
│ GO API (:8080) │
│ │
│ Gin Router → Middleware → Handlers → Services │
│ │
│ Middleware: CORS, Auth (JWT), Logger, Recovery │
│ Handlers: Thin HTTP layer, request/response only │
│ Services: Business logic, DB queries, validation │
└────┬─────────────┬───────────┬──────────┬──────────┬───────┘
│ │ │ │ │
│ │ │ │ │
┌────┴────┐ ┌────┴────┐ ┌───┴───┐ ┌───┴───┐ ┌───┴───┐
│PostgreSQL│ │ Redis │ │ MinIO │ │ Resend │ │ Jobs │
│ :5432 │ │ :6379 │ │ :9000│ │ API │ │ (asynq)│
└─────────┘ └────────┘ └───────┘ └───────┘ └───────┘Go API Layer
The backend is a Go application built with Gin (HTTP router) and GORM (ORM). It follows a layered architecture: routes register handlers, handlers call services, and services interact with the database through GORM models.
apps/api/
├── cmd/server/main.go # Entry point: loads config, connects DB,
│ # registers routes, starts Gin server
└── internal/
├── config/config.go # Reads .env, returns typed config struct
├── database/database.go # GORM connection, auto-migration
├── models/ # GORM structs (User, Post, etc.)
├── handlers/ # Gin handlers (HTTP request/response)
├── services/ # Business logic (queries, validation)
├── middleware/ # Auth, CORS, logging, rate limiting
├── routes/routes.go # Route registration + GORM Studio mount
├── mail/ # Resend email client + HTML templates
├── storage/ # S3-compatible file storage abstraction
├── jobs/ # Background job queue (asynq + Redis)
├── cron/ # Cron scheduler (asynq scheduler)
├── cache/ # Redis caching layer
└── ai/ # AI provider abstraction (Claude, OpenAI)Handler / Service Separation
Handlers are thin -- they parse HTTP requests, call a service method, and return a JSON response. All business logic, database queries, and validation live in the service layer. This separation makes code testable and reusable.
// Handler: thin HTTP layer
func (h *PostHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
search := c.Query("search")
sortBy := c.DefaultQuery("sort_by", "created_at")
items, total, pages, err := h.Service.List(params)
if err != nil {
c.JSON(500, gin.H{"error": gin.H{"code": "INTERNAL_ERROR", ...}})
return
}
c.JSON(200, gin.H{
"data": items,
"meta": gin.H{"total": total, "page": page, "pages": pages},
})
}// Service: business logic + database queries
func (s *PostService) List(params PostListParams) ([]models.Post, int64, int, error) {
query := s.DB.Model(&models.Post{})
if params.Search != "" {
query = query.Where("title ILIKE ? OR content ILIKE ?",
"%"+params.Search+"%", "%"+params.Search+"%")
}
var total int64
query.Count(&total)
var items []models.Post
offset := (params.Page - 1) * params.PageSize
query.Order(params.SortBy + " " + params.SortOrder).
Offset(offset).Limit(params.PageSize).Find(&items)
pages := int(math.Ceil(float64(total) / float64(params.PageSize)))
return items, total, pages, nil
}Middleware Stack
Every request passes through a middleware chain before reaching a handler. Grit ships with four built-in middleware layers:
| Middleware | Purpose |
|---|---|
| Recovery | Catches panics and returns a 500 JSON error instead of crashing |
| CORS | Allows cross-origin requests from the frontend origins (localhost:3000, :3001) |
| Logger | Structured JSON logging with method, path, status, and duration |
| Auth (JWT) | Validates JWT tokens, attaches user to context, enforces roles |
Route Groups
The Go API organizes routes into three groups with different levels of authentication and authorization:
| Group | Auth | Example Routes |
|---|---|---|
| public | None | /api/health, /api/auth/login, /api/auth/register |
| protected | Valid JWT | /api/posts, /api/users/:id, /api/auth/me |
| admin | JWT + admin role | DELETE /api/users/:id, DELETE /api/posts/:id |
When the code generator creates a new resource, it automatically injects CRUD routes into the protected group (list, get, create, update) and the delete route into the admin group.
Next.js Frontend Layer
Both the web app and admin panel are Next.js 14+ applications using the App Router. They share the same data-fetching pattern: React Query hooks call the Go API through an Axios-based API client that automatically injects JWT tokens.
Web App (apps/web)
The main customer-facing frontend. Ships with authentication pages (login, register, forgot-password), a protected dashboard layout with a sidebar, and React Query hooks for data fetching. Styled with Tailwind CSS and shadcn/ui components.
Admin Panel (apps/admin)
A Filament-inspired admin dashboard. Developers define resources in TypeScript (columns, form fields, filters, actions) and get full CRUD pages with data tables, forms, and dashboard widgets. The admin panel features:
- ✓Resource-based page generation from TypeScript definitions
- ✓Server-side paginated DataTable with sorting, filtering, and search
- ✓Form builder with text, number, select, date, toggle, and textarea fields
- ✓Dashboard with stats cards, charts (Recharts), and recent activity widgets
- ✓Auto-generated sidebar navigation from registered resources
- ✓Dark theme matching the GORM Studio aesthetic
Data Fetching Pattern
All API communication flows through React Query hooks. Components never call fetch or Axios directly. This ensures consistent caching, background refetching, loading states, and cache invalidation on mutations.
// Generated React Query hook
export function usePosts({ page, search, sortBy, sortOrder } = {}) {
return useQuery<PostsResponse>({
queryKey: ["posts", { page, search, sortBy, sortOrder }],
queryFn: async () => {
const { data } = await apiClient.get(`/api/posts?${params}`);
return data;
},
});
}
export function useCreatePost() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input) => apiClient.post("/api/posts", input),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["posts"] });
},
});
}Shared Package
The packages/shared directory is the bridge between the Go backend and the TypeScript frontends. It contains three categories of shared code:
| Directory | Contents | Example |
|---|---|---|
| schemas/ | Zod validation schemas | CreatePostSchema, UpdatePostSchema |
| types/ | TypeScript interfaces | interface Post { id: number; title: string; ... } |
| constants/ | Shared constants | API_ROUTES, ROLES, APP_CONFIG |
When you run grit generate resource or grit sync, these files are auto-generated from your Go models, keeping the frontend in sync with the backend.
Request Flow
Here is the complete path of a typical authenticated API request, from the user clicking a button to the data appearing on screen:
User interaction
User clicks "Save" in the admin panel form. React calls the useCreatePost() mutation hook.
API client
The hook calls apiClient.post("/api/posts", data). Axios automatically attaches the JWT access token from localStorage to the Authorization header.
CORS middleware
The Go API receives the request. CORS middleware validates the origin (localhost:3001) and allows it.
Auth middleware
JWT middleware extracts the token, validates its signature and expiry, and attaches the authenticated user to the Gin context.
Handler
The PostHandler.Create handler parses the JSON body using ShouldBindJSON, validates required fields via struct binding tags, and calls the service.
Service
The PostService.Create method runs business logic (if any), then calls GORM to insert the record into PostgreSQL.
Database
GORM translates the struct into an INSERT query, executes it against PostgreSQL, and populates the ID and timestamps.
Response
The handler returns a JSON response: { "data": { ... }, "message": "Post created successfully" } with HTTP 201.
Cache invalidation
React Query receives the response, triggers onSuccess, and invalidates the ["posts"] query key. The list refetches automatically.
GORM Studio
GORM Studio is a visual database browser embedded directly into the Go API. It mounts at /studio and gives you a web UI to browse, query, and inspect your database tables. All registered GORM models appear automatically.
// GORM Studio is mounted with all registered models
studio.Mount(router, db, []interface{}{
&models.User{},
&models.Post{}, // auto-injected by grit generate
/* grit:studio */
}, studio.Config{Prefix: "/studio"})When you generate a new resource, the CLI automatically injects the model into the GORM Studio mount call using the /* grit:studio */ marker.
API Response Format
All API endpoints follow a consistent JSON response format. This predictability allows React Query hooks and error handlers to work generically across every resource.
{
"data": { ... },
"message": "Post created"
}{
"data": [ ... ],
"meta": {
"total": 100,
"page": 1,
"page_size": 20,
"pages": 5
}
}{
"error": {
"code": "VALIDATION_ERROR",
"message": "Title is required",
"details": { "title": "This field is required" }
}
}Authentication Flow
Grit uses JWT (JSON Web Tokens) for authentication. The flow works as follows:
- User submits login form with email and password
- Go API validates credentials and returns an access token (15 min) and refresh token (7 days)
- Frontend stores tokens in localStorage and attaches the access token to every API request via Axios interceptor
- When the access token expires, the Axios interceptor catches the 401, calls /api/auth/refresh with the refresh token, gets a new access token, and retries the original request
- Protected routes in the frontend check for a valid token and redirect to /login if missing
- The Go API auth middleware validates the token signature, checks expiry, and attaches the user to the Gin context
- Role-based middleware (RequireRole("admin")) checks the user's role from the context
Infrastructure Services
Docker Compose provides all the infrastructure services needed for local development. In production, you can swap these for managed services (e.g., AWS RDS for PostgreSQL, ElastiCache for Redis, S3 for file storage).
| Service | Port | Purpose |
|---|---|---|
| PostgreSQL 16 | 5432 | Primary database (GORM auto-migrates on startup) |
| Redis 7 | 6379 | Caching, session storage, job queue backend (asynq) |
| MinIO | 9000 / 9001 | S3-compatible object storage for file uploads |
| Mailhog | 1025 / 8025 | Local email testing (SMTP on 1025, web UI on 8025) |