Core Concepts

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.

project structure
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/ structure
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.

internal/handlers/post.go
// 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},
    })
}
internal/services/post.go
// 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:

MiddlewarePurpose
RecoveryCatches panics and returns a 500 JSON error instead of crashing
CORSAllows cross-origin requests from the frontend origins (localhost:3000, :3001)
LoggerStructured 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:

GroupAuthExample Routes
publicNone/api/health, /api/auth/login, /api/auth/register
protectedValid JWT/api/posts, /api/users/:id, /api/auth/me
adminJWT + admin roleDELETE /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.

hooks/use-posts.ts
// 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:

DirectoryContentsExample
schemas/Zod validation schemasCreatePostSchema, UpdatePostSchema
types/TypeScript interfacesinterface Post { id: number; title: string; ... }
constants/Shared constantsAPI_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:

1

User interaction

User clicks "Save" in the admin panel form. React calls the useCreatePost() mutation hook.

2

API client

The hook calls apiClient.post("/api/posts", data). Axios automatically attaches the JWT access token from localStorage to the Authorization header.

3

CORS middleware

The Go API receives the request. CORS middleware validates the origin (localhost:3001) and allows it.

4

Auth middleware

JWT middleware extracts the token, validates its signature and expiry, and attaches the authenticated user to the Gin context.

5

Handler

The PostHandler.Create handler parses the JSON body using ShouldBindJSON, validates required fields via struct binding tags, and calls the service.

6

Service

The PostService.Create method runs business logic (if any), then calls GORM to insert the record into PostgreSQL.

7

Database

GORM translates the struct into an INSERT query, executes it against PostgreSQL, and populates the ID and timestamps.

8

Response

The handler returns a JSON response: { "data": { ... }, "message": "Post created successfully" } with HTTP 201.

9

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.

internal/routes/routes.go
// 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.

success (single item)
{
  "data": { ... },
  "message": "Post created"
}
success (paginated list)
{
  "data": [ ... ],
  "meta": {
    "total": 100,
    "page": 1,
    "page_size": 20,
    "pages": 5
  }
}
error
{
  "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:

  1. User submits login form with email and password
  2. Go API validates credentials and returns an access token (15 min) and refresh token (7 days)
  3. Frontend stores tokens in localStorage and attaches the access token to every API request via Axios interceptor
  4. 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
  5. Protected routes in the frontend check for a valid token and redirect to /login if missing
  6. The Go API auth middleware validates the token signature, checks expiry, and attaches the user to the Gin context
  7. 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).

ServicePortPurpose
PostgreSQL 165432Primary database (GORM auto-migrates on startup)
Redis 76379Caching, session storage, job queue backend (asynq)
MinIO9000 / 9001S3-compatible object storage for file uploads
Mailhog1025 / 8025Local email testing (SMTP on 1025, web UI on 8025)