Architecture Modes

Double Architecture: Web + API

A streamlined Turborepo monorepo with two applications: a public-facing web app and a Go API. Admin features live inside the web app as role-protected routes instead of a separate application.

Overview

Double architecture strips away the dedicated admin panel and gives you a two-app monorepo. The Go API handles all backend logic (authentication, CRUD, file uploads, jobs), and a single frontend serves both regular users and administrators. Admin functionality is implemented through role-protected routes within the web app — users with the ADMIN role see additional navigation items and have access to management pages.

scaffold command
# Double with TanStack Router (recommended)
grit new myapp --double --vite
# Double with Next.js
grit new myapp --double --next

Best suited for

  • - Projects where admins and users share the same interface with extra privileges
  • - Blogs, portfolios, and content sites where the author manages content inline
  • - Simpler SaaS applications where admin features are minimal (user management, settings)
  • - MERN/MEAN stack developers transitioning to Go who want a familiar two-app structure
  • - Teams that want fewer apps to deploy and maintain

Key differences from Triple

  • - No apps/admin/ directory — the admin panel does not exist as a separate application
  • - No admin resource definitions — no defineResource(), no DataTable/FormBuilder auto-generation
  • - Admin features in web app — role-protected routes (e.g., /admin/users) guarded by RequireRole("ADMIN")
  • - Fewer generated filesgrit generate creates Go files + shared types + web hooks, but no admin page or resource definition
  • - Simpler deployment — 2 apps instead of 3, fewer Docker images, less infrastructure

Complete Folder Structure

This is the full tree generated by grit new myapp --double --vite. Compare it with the triple structure — the apps/admin/ directory is absent, and the web app gains role-protected admin routes.

myapp/
myapp/
├── .env # Shared environment variables
├── .env.example # Template for other developers
├── .gitignore
├── .prettierrc # Code formatting
├── .prettierignore
├── docker-compose.yml # Dev: PostgreSQL, Redis, MinIO, Mailhog
├── docker-compose.prod.yml # Production deployment
├── grit.json # Project manifest (architecture: "double")
├── turbo.json # Turborepo pipeline config
├── pnpm-workspace.yaml # pnpm workspace definition
├── package.json # Root scripts (dev, build, lint)
├── .claude/
│ └── skills/grit/
│ ├── SKILL.md # AI assistant guide (tailored to double)
│ └── reference.md # Detailed API conventions
├── packages/
│ └── shared/ # Shared between API and web
│ ├── package.json
│ ├── schemas/ # Zod validation schemas
│ │ ├── index.ts # // grit:schemas marker
│ │ └── user.ts # User schema (register, login, update)
│ ├── types/ # TypeScript interfaces
│ │ ├── index.ts # // grit:types marker
│ │ └── user.ts # User type
│ └── constants/ # Shared constants
│ └── index.ts # API_ROUTES, ROLES // grit:api-routes
├── apps/
│ ├── api/ # Go backend (identical to triple)
│ │ ├── Dockerfile # Multi-stage build (golang → alpine)
│ │ ├── go.mod # Go module: myapp/apps/api
│ │ ├── go.sum
│ │ ├── cmd/
│ │ │ ├── server/main.go # Entry point: config, db, services, router
│ │ │ ├── migrate/main.go # Migration runner
│ │ │ └── seed/main.go # Database seeder
│ │ └── internal/
│ │ ├── config/config.go # Loads .env, all env vars
│ │ ├── database/db.go # GORM connection + AutoMigrate
│ │ ├── models/ # GORM models
│ │ │ ├── user.go # User model // grit:models marker
│ │ │ └── upload.go # Upload model
│ │ ├── handlers/ # HTTP handlers (thin — call services)
│ │ │ ├── auth.go # Register, login, refresh, me
│ │ │ ├── user.go # User CRUD
│ │ │ ├── upload.go # File upload (presigned URLs)
│ │ │ └── ai.go # AI completions + streaming
│ │ ├── services/ # Business logic
│ │ │ ├── auth_service.go # JWT, bcrypt, token generation
│ │ │ └── user_service.go # User queries
│ │ ├── middleware/ # Gin middleware
│ │ │ ├── auth.go # JWT verification
│ │ │ ├── cors.go # CORS configuration
│ │ │ ├── logger.go # Structured logging
│ │ │ ├── cache.go # Redis response caching
│ │ │ ├── maintenance.go # grit down/up support
│ │ │ └── rate_limit.go # Sentinel rate limiting
│ │ ├── routes/
│ │ │ └── routes.go # Route registration // grit:handlers, grit:routes:*
│ │ ├── mail/ # Email service (Resend)
│ │ │ ├── mailer.go # Send function
│ │ │ └── templates/ # HTML email templates
│ │ ├── storage/ # S3-compatible file storage
│ │ ├── jobs/ # Background jobs (asynq)
│ │ ├── cron/ # Scheduled tasks
│ │ ├── cache/ # Redis cache service
│ │ ├── ai/ # AI service (Vercel AI Gateway)
│ │ └── auth/ # TOTP 2FA service
│ │ └── totp.go # Setup, verify, backup codes, trusted devices
│ └── web/ # Frontend (public + admin routes)
│ ├── Dockerfile # Next.js standalone or Vite static build
│ ├── package.json # Dependencies + scripts
│ ├── vite.config.ts # (or next.config.js for --next)
│ ├── tailwind.config.ts
│ └── src/ # TanStack Router (or app/ for Next.js)
│ ├── routes/
│ │ ├── __root.tsx # Root layout
│ │ ├── index.tsx # Landing page
│ │ ├── (auth)/ # Auth pages (login, register)
│ │ ├── (app)/ # Protected user pages
│ │ └── (admin)/ # Role-protected admin pages
│ │ ├── _layout.tsx # Admin layout (checks ADMIN role)
│ │ ├── users/ # User management
│ │ └── settings/ # System settings
│ ├── hooks/ # React Query hooks
│ ├── components/ # Shared components
│ └── lib/ # Utilities, API client
└── packages/
└── grit-ui/ # 100 shadcn-compatible components
├── registry.json
└── registry/ # Per-component JSON + TSX

Directory-by-Directory Breakdown

Root Files

The root configuration is nearly identical to triple. The key difference is in grit.json which stores "architecture": "double", telling the CLI to skip admin-specific file generation.

FilePurpose
.envShared environment variables: DB connection, JWT secret, Redis URL, S3 credentials, Resend API key
grit.jsonProject manifest. Architecture is set to double — the CLI uses this to determine which files to generate
turbo.jsonTurborepo pipelines: dev, build, lint, test. Simpler dependency graph with only 2 apps
docker-compose.ymlDevelopment infrastructure: PostgreSQL 16, Redis 7, MinIO, Mailhog
docker-compose.prod.ymlProduction deployment with 2 app containers (API + web) instead of 3

packages/shared/

Identical to the triple architecture. Contains Zod schemas, TypeScript types, and shared constants. The only difference is that there is one consumer (web) instead of two (web + admin). The marker comments and injection system work the same way.

DirectoryContents
schemas/Zod validation schemas per resource. Re-exported from index.ts via the // grit:schemas marker.
types/TypeScript interfaces per resource. Re-exported via // grit:types marker.
constants/API route constants and role enums. Injected via // grit:api-routes marker.

apps/api/ (Go Backend)

The Go API is identical to the triple architecture. It uses the same handler-service-model pattern, the same middleware stack, the same batteries (cache, storage, email, jobs, cron, AI). The only difference is that the CORS middleware is configured to allow a single frontend origin instead of two.

DirectoryRole
cmd/server/Entry point: loads config, connects to DB, starts Gin on :8080
cmd/migrate/Standalone migration runner via GORM AutoMigrate
cmd/seed/Database seeder with admin user and sample data
internal/config/Typed config struct loaded from .env
internal/database/GORM connection: PostgreSQL (production) or SQLite (dev/test)
internal/models/GORM model definitions with struct tags. // grit:models marker for injection.
internal/handlers/Thin Gin handlers: parse input, call service, return JSON
internal/services/Business logic: CRUD with pagination, filtering, error handling
internal/middleware/JWT auth, CORS, logger, cache, rate limiter, maintenance mode
internal/routes/Centralized route registration with marker comments
internal/mail/Resend email service with HTML templates
internal/storage/S3-compatible file storage with presigned URLs
internal/jobs/Background job processing with asynq + Redis
internal/cron/Scheduled tasks with cron expressions
internal/cache/Redis cache service with TTL
internal/ai/Claude + OpenAI with streaming
internal/auth/TOTP 2FA: setup, verify, backup codes, trusted devices

apps/web/ (Public + Admin Frontend)

In double architecture, the web app serves double duty. Public pages are accessible to everyone, while admin routes are protected by role checks. The navigation dynamically shows admin links (e.g., "Users", "Settings") only when the logged-in user has the ADMIN role.

PathDescription
src/routes/__root.tsxRoot layout: providers, fonts, global styles (TanStack Router). For Next.js: app/layout.tsx
src/routes/index.tsxLanding page visible to all visitors
src/routes/(auth)/Auth route group: login, register, forgot-password
src/routes/(app)/Protected user pages: dashboard, profile, resource pages
src/routes/(admin)/Role-protected admin routes. The layout component checks the user role and redirects non-admins. Contains user management, system settings, and other admin-only pages.
src/hooks/React Query hooks for data fetching (generated by grit generate)
src/lib/Utilities: API client (Axios/fetch wrapper), auth helpers, formatters

How role-protected admin routes work

src/routes/(admin)/_layout.tsx
// The admin layout checks the user's role before rendering children
import { useAuth } from '@/hooks/use-auth'
import { Navigate, Outlet } from '@tanstack/react-router'
export default function AdminLayout() {
const { user } = useAuth()
// Redirect non-admin users
if (user?.role !== 'ADMIN') {
return <Navigate to="/" />
}
return (
<div className="flex">
{/* Admin sidebar with management links */}
<AdminSidebar />
<main className="flex-1 p-6">
<Outlet />
</main>
</div>
)
}

The backend also enforces role checks via the RequireRole("ADMIN") middleware on admin API routes, so the frontend check is for UX only — not a security boundary.

Data Flow

The double architecture has a simpler data flow with a single frontend talking to the API. Admin and user requests follow the same path — the only difference is the role attached to the JWT token.

request flow
┌─────────────────────────────────────────────────────────┐
│ BROWSER │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Web App (:3000) │ │
│ │ ┌──────────────┐ ┌──────────────────────────┐ │ │
│ │ │ Public Pages │ │ Admin Pages (role-gated) │ │ │
│ │ │ /, /login, │ │ /admin/users, │ │ │
│ │ │ /dashboard │ │ /admin/settings │ │ │
│ │ └──────┬───────┘ └────────────┬─────────────┘ │ │
│ └─────────┼────────────────────────┼─────────────────┘ │
└────────────┼────────────────────────┼───────────────────┘
│ │
REST + JWT REST + JWT (ADMIN role)
│ │
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ GO API (:8080) │
│ │
│ Request → Gin Router │
│ → Middleware Stack │
│ ├── CORS (allow web:3000) │
│ ├── Logger │
│ ├── Rate Limiter (Sentinel) │
│ ├── Auth (JWT verification) │
│ ├── RequireRole("ADMIN") ← admin routes only │
│ └── Cache (Redis) │
│ → Handler → Service → GORM → PostgreSQL │
│ │
│ Background: asynq workers, cron, GORM Studio │
└─────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
PostgreSQL Redis MinIO Resend
(data) (cache (files) (email)
+ jobs)

Step-by-step: Admin managing users

  1. Admin navigates to /admin/users in the web app
  2. The admin layout component verifies the user's role is ADMIN (frontend guard)
  3. React Query hook calls GET /api/v3/admin/users with the JWT token
  4. Gin router matches the admin route group and applies RequireRole("ADMIN") middleware (backend guard)
  5. Handler calls userService.GetAll() and returns the paginated user list
  6. The web app renders the user table — built with your own components (not the auto-generated DataTable from triple)

How Code Generation Works

In double architecture, grit generate creates fewer files compared to triple because there is no admin panel to scaffold into. The Go backend files and shared types are identical — the difference is the absence of admin resource definitions and admin pages.

example command
grit generate resource Product --fields "name:string, price:float, description:text, category_id:belongs_to, is_active:bool"

Files Created

Go Backend (apps/api/)

  • - internal/models/product.go — GORM model with struct tags, belongs_to relationship
  • - internal/services/product_service.go — CRUD operations with pagination
  • - internal/handlers/product.go — Gin handlers: Create, GetAll, GetByID, Update, Delete

Shared Package (packages/shared/)

  • - schemas/product.ts — Zod schemas: CreateProductSchema, UpdateProductSchema
  • - types/product.ts — TypeScript interface: Product

Web App (apps/web/)

  • - hooks/use-products.ts — React Query hooks: useProducts(), useProduct(id), useCreateProduct(), useUpdateProduct(), useDeleteProduct()

NOT generated (compared to triple)

  • - resources/products.ts — No admin resource definition
  • - app/(dashboard)/products/page.tsx — No auto-generated admin page with DataTable and FormBuilder

To manage products as an admin in double architecture, you build the management UI yourself inside the src/routes/(admin)/ route group, using the generated React Query hooks and shared types.

Marker Injections

Double uses 5 marker injections instead of 6 (no admin resource marker).

FileMarkerWhat Gets Injected
routes/routes.go// grit:handlersHandler initialization
routes/routes.go// grit:routes:*Route group registration
schemas/index.ts// grit:schemasSchema re-export
types/index.ts// grit:typesType re-export
constants/index.ts// grit:api-routesAPI route constant

Ports & URLs

With one fewer frontend, the port layout is simpler. No port 3001 is occupied.

ServicePortURLNotes
Go API8080http://localhost:8080REST endpoints + GORM Studio at /studio
Web App3000http://localhost:3000Next.js or Vite (:5173) — serves both public and admin UI
PostgreSQL5432localhost:5432Docker container
Redis6379localhost:6379Cache + job queue
MinIO API9000http://localhost:9000S3-compatible API
MinIO Console9001http://localhost:9001Web UI for managing buckets
Mailhog SMTP1025localhost:1025Catches outgoing email
Mailhog UI8025http://localhost:8025View caught emails in browser

Deployment Options

Deploying double is simpler than triple — two containers instead of three, one frontend domain instead of two.

Docker Compose (Self-hosted)

The production Docker Compose file builds the Go API and the web app as optimized images. With only 2 app containers, resource usage is lower and the configuration is simpler.

# Build and deploy
docker compose -f docker-compose.prod.yml up -d --build

grit deploy

Works the same as triple — grit deploy reads the grit.json manifest to determine which apps to build and deploy. With double architecture, it skips the admin container entirely.

Hybrid: Vercel + VPS

Deploy the web app (Next.js) to Vercel and the Go API to a VPS. Simpler than triple because you only have one Vercel project to configure instead of two. SetNEXT_PUBLIC_API_URL in Vercel to point to your API server.

Static Deploy (Vite only)

If you chose --vite, the web app builds to static files that can be served from any CDN (Cloudflare Pages, Netlify, S3 + CloudFront). The Go API is the only server-side component. This is the most cost-effective deployment option.

When to Choose Double

Double is the right choice when you want the power of a monorepo with shared types but don't need a separate admin application.

Choose Double when...

  • - Admins and users share the same UI with role-based visibility
  • - Your admin features are simple (user list, settings page)
  • - You are building a blog, portfolio, or personal SaaS
  • - You want fewer apps to deploy and maintain
  • - You prefer building admin UI manually with your own components
  • - You are familiar with MERN/MEAN stack structure (API + SPA)

Consider Triple instead when...

  • - You need DataTable + FormBuilder auto-generated admin pages
  • - Non-technical admins need a polished, dedicated dashboard
  • - You want grit generate to create admin pages automatically
  • - Your admin operations are complex (multi-step forms, widgets, analytics)
  • - Separate teams work on the public site vs admin panel

Upgrading from Double to Triple

If your project outgrows double architecture, you can upgrade to triple later. Rungrit upgrade --triple to scaffold the admin app into your existing project. Your existing Go API, shared types, and web app remain untouched — the command only adds the apps/admin/ directory and updates grit.json.

Example Project

The same Job Portal application built with double architecture. The web app includes role-protected admin routes for managing job listings, companies, and applications. Compare it with the triple example to see the structural differences.

Job Portal — Double + TanStack Router

Full source code with README, .env template, step-by-step guide, and production Docker Compose.

View on GitHub →