Grit Desktop — Complete LLM Reference
The canonical guide for AI assistants building native desktop applications with Grit Desktop. Covers architecture, every CLI command, resource generation, field types, Wails bindings, DataTable, FormBuilder, code markers, building executables, and the rules that must never be broken.
For AI tools: Read this entire page before generating any code for Grit Desktop projects. Every convention here is enforced by the CLI — deviating breaks grit generate and grit remove. Desktop uses Wails bindings, not HTTP — there is no REST API.
1What is Grit Desktop?
Grit Desktop builds native desktop applications using Go + Wails + React. One command (grit new-desktop myapp) scaffolds a complete project that compiles to a single native binary. No browser, no Electron, no separate runtime.
Backend
Go (Wails v2 bindings) — direct function calls, no HTTP server
Frontend
Vite + React + TanStack Router (file-based) + TanStack Query + Tailwind CSS
Database
SQLite (default, local) via GORM — fully offline-first
Distribution
Single binary (~10-15MB) for Windows, macOS, and Linux
Code Generation
grit generate resource — same CLI, auto-detects desktop
Dev Tools
GORM Studio (localhost:8080/studio), hot-reload, PDF/Excel export
The scaffolded project includes JWT authentication, blog and contact CRUD resources, PDF and Excel data export, a custom frameless title bar, dark theme with shadcn/ui components, toast notifications, and GORM Studio for database browsing — all out of the box.
2Why Wails (not Electron or Tauri)?
Grit chose Wails because Go is already the backend language for Grit web projects. Using Wails means developers write Go for both web and desktop — no Rust learning curve (Tauri), and none of Electron's bloat.
| Aspect | Wails | Electron | Tauri |
|---|---|---|---|
| Backend language | Go | JavaScript/C++ | Rust |
| Binary size | ~10-15MB | ~150MB+ | ~5-10MB |
| RAM usage | ~30-50MB | ~200MB+ | ~30-50MB |
| Frontend | Any web framework | Any web framework | Any web framework |
| Backend calls | Direct Go bindings | IPC (serialization overhead) | Rust commands (compile-time checked) |
| Learning curve | Go + React | JavaScript only | Rust + JavaScript |
| Startup time | <1s | 2-5s | <1s |
| Cross-platform | Win / macOS / Linux | Win / macOS / Linux | Win / macOS / Linux |
3Architecture
Unlike Grit web projects (which use Gin HTTP server), desktop projects have no HTTP server. Wails bridges Go and JavaScript directly. The React frontend calls Go functions through generated TypeScript bindings.
There is no REST API in desktop projects. React calls Go functions directly via window.go.main.App.MethodName(). Never suggest HTTP endpoints, fetch calls, or Axios for desktop projects.
Call Flow
React Component--> Wails TypeScript Binding (auto-generated)--> Go App Method (on the App struct)--> GORM Service (business logic + queries)--> SQLite Database
App Struct (app.go)
A Go struct with exported methods. Any method on this struct with exported name and supported types is automatically available to the React frontend. Wails generates TypeScript bindings during build.
React Frontend (frontend/)
A Vite-powered React app with TanStack Router (file-based routing) and TanStack Query for state management. Calls Go functions via the generated Wails bindings — no fetch, no Axios, no HTTP.
GORM Services (internal/service/)
Business logic and database queries. Each resource has a service with List, ListAll, GetByID, Create, Update, Delete methods. Services operate on GORM models backed by SQLite.
SQLite Database
Local file-based database (app.db). All data lives on the user's machine. Fully offline — no network connection required.
4Project Structure
Desktop projects are a single directory (not a monorepo). Go code lives at the root and in internal/, while the React frontend lives in frontend/.
myapp/├── main.go # Wails entry point — creates app, binds methods├── app.go # App struct with all bound methods + constructor├── types.go # Input structs for bound methods├── wails.json # Wails project configuration├── go.mod # Go module definition├── go.sum # Go dependency checksums│├── internal/│ ├── config/│ │ └── config.go # App configuration (reads .env)│ ├── db/│ │ └── db.go # GORM database setup (SQLite) + AutoMigrate│ ├── models/│ │ ├── user.go # User model (auth)│ │ ├── blog.go # Blog post model (scaffolded example)│ │ └── contact.go # Contact model (scaffolded example)│ └── service/│ ├── auth.go # Authentication service (register, login, JWT)│ ├── blog.go # Blog CRUD service│ └── contact.go # Contact CRUD service│├── frontend/│ ├── src/│ │ ├── main.tsx # React entry point (TanStack Router)│ │ ├── routes/ # File-based routes (TanStack Router)│ │ │ ├── __root.tsx # Root route│ │ │ ├── _layout.tsx # Auth guard + sidebar layout│ │ │ └── _layout/ # Protected page routes│ │ ├── components/ # Shared UI: sidebar, title-bar, data-table, etc.│ │ │ └── sidebar.tsx # App sidebar with navigation│ │ ├── hooks/ # TanStack Query hooks for Wails bindings│ │ └── lib/ # Utilities│ ├── index.html│ ├── package.json│ ├── vite.config.ts│ └── tailwind.config.js│├── build/│ ├── appicon.png # App icon (1024x1024 — auto-converted per platform)│ └── bin/ # Build output directory│└── cmd/└── studio/└── main.go # GORM Studio server (port 8080)
Key difference from web projects: Desktop has app.go + types.go at the root instead of handlers/ and routes/. There is no apps/ directory, no Turborepo, no monorepo structure.
5All CLI Commands
Every desktop-relevant command. The CLI auto-detects desktop projects by checking for wails.json.
| Command | Description |
|---|---|
| grit new-desktop <name> | Scaffold a complete desktop project with Go backend, React frontend, auth, CRUD, GORM Studio, and dark theme |
| grit generate resource <Name> --fields "..." | Generate a full-stack CRUD resource: Go model, service, React list page, React form page, plus 10 code injections |
| grit remove resource <Name> | Remove a generated resource: deletes files and reverses all 10 injections. Markers stay intact. |
| grit start | Start development mode (runs wails dev). Opens native window with hot-reload for Go and React. |
| grit compile | Build native executable (runs wails build). Output in build/bin/. |
| grit studio | Open GORM Studio at localhost:8080/studio — visual database browser. |
| grit version | Show installed CLI version. |
Desktop projects do not have grit start server or grit start client. Those are web-only. For desktop, use grit start or wails dev directly. There is no grit migrate or grit seed for desktop — GORM AutoMigrate runs on startup automatically.
6Resource Generation — Complete Reference
The same grit generate resource command works for both web and desktop projects. The CLI auto-detects the project type by checking for wails.json.
Syntax
Supported Field Types
| Type | Go Type | Form Input | Notes |
|---|---|---|---|
| string | string | Text input | Short text — titles, names, emails |
| text | string | Textarea | Long text — descriptions, notes |
| richtext | string | Textarea | Rich content (rendered as textarea in desktop) |
| int | int | Number input | Signed integer — stock, quantity |
| uint | uint | Number input | Unsigned integer — IDs, counts |
| float | float64 | Number input (decimal) | Prices, coordinates, ratings |
| bool | bool | Toggle switch | Active, published, featured flags |
| date | time.Time | Date picker | Date only — due dates, birthdays |
| datetime | time.Time | DateTime picker | Date + time — event timestamps |
| slug | string (uniqueIndex) | Auto-generated (excluded from form) | URL-friendly slug from source field |
| belongs_to | uint (foreign key) | Number input (FK ID) | Foreign key to parent model |
Slug Field Syntax
The slug type uses a special syntax to specify the source field:
This adds a uniqueIndex and a GORM BeforeCreate hook that auto-generates the slug from the title. Slug fields are excluded from forms and input structs.
Files Created Per Resource
internal/models/<snake>.goGORM model struct with ID, fields, timestamps, and soft delete. Includes slug hook if applicable.
internal/service/<snake>.goService with List (paginated), ListAll, GetByID, Create, Update, Delete methods. Returns PaginatedResult for list queries.
frontend/src/routes/_layout/<plural>.index.tsxList route — DataTable, search, pagination, PDF/Excel export
frontend/src/routes/_layout/<plural>.new.tsxCreate form route — field-type-based inputs, validation, toast
frontend/src/routes/_layout/<plural>.$id.edit.tsxEdit form route — pre-fills from GetByID, type-safe params
Example: Generate a Product Resource
Generated Model
package modelsimport ("gorm.io/gorm")type Product struct {gorm.ModelName string `gorm:"not null" json:"name"`Price float64 `json:"price"`Stock int `json:"stock"`Published bool `gorm:"default:false" json:"published"`}type ProductInput struct {Name string `json:"name"`Price float64 `json:"price"`Stock int `json:"stock"`Published bool `json:"published"`}
Generated Service (Skeleton)
package serviceimport ("myapp/internal/models""gorm.io/gorm")type ProductService struct {DB *gorm.DB}type PaginatedResult struct {Data interface{} `json:"data"`Total int64 `json:"total"`Page int `json:"page"`Pages int `json:"pages"`}func (s *ProductService) List(page, pageSize int, search string) (*PaginatedResult, error) { ... }func (s *ProductService) ListAll() ([]models.Product, error) { ... }func (s *ProductService) GetByID(id uint) (*models.Product, error) { ... }func (s *ProductService) Create(input models.ProductInput) (*models.Product, error) { ... }func (s *ProductService) Update(id uint, input models.ProductInput) (*models.Product, error) { ... }func (s *ProductService) Delete(id uint) error { ... }
7DataTable (List Page)
Every generated resource gets a list page with a full-featured DataTable. Here is what it includes out of the box:
Customizing Columns
Columns are defined in the list page index.tsx. You can reorder, rename, add custom formatting, or add computed columns:
const columns = [{ accessorKey: "name", header: "Product Name" },{accessorKey: "price",header: "Price",cell: ({ row }: any) => "$" + Number(row.getValue("price")).toFixed(2),},{ accessorKey: "stock", header: "Stock" },{accessorKey: "published",header: "Status",cell: ({ row }: any) =>row.getValue("published") ? "Published" : "Draft",},];
8FormBuilder (Form Page)
Every generated resource gets a form page that handles both creating and editing records. The form fields are determined by the field types declared during generation.
Field Type to Form Input Mapping
| Field Type | Form Input | Behavior |
|---|---|---|
| string | Text input (<input type="text">) | Standard text field with label |
| text / richtext | Textarea (<textarea>) | Multi-line text area, resizable |
| int / uint / float | Number input (<input type="number">) | Numeric input with step control |
| bool | Toggle switch | On/off toggle, defaults to false |
| date | Date picker | Calendar date selector |
| datetime | DateTime picker | Calendar + time selector |
| slug | (Excluded from form) | Auto-generated in Go service from source field |
| belongs_to | Number input (FK ID) | Enter the related record ID directly |
Create vs Edit Mode
Form validation happens on the Go side. If a GORM not null constraint fails or a service returns an error, the React form displays a toast notification with the error message.
9Wails Bound Methods
Go methods on the App struct are automatically available in React. Wails generates TypeScript bindings during wails dev and wails build. React calls them as async functions.
7 Methods Generated Per Resource
// List with pagination and searchfunc (a *App) GetProducts(page, pageSize int, search string) (*service.PaginatedResult, error)// Get single record by IDfunc (a *App) GetProduct(id uint) (*models.Product, error)// Create new recordfunc (a *App) CreateProduct(input models.ProductInput) (*models.Product, error)// Update existing recordfunc (a *App) UpdateProduct(id uint, input models.ProductInput) (*models.Product, error)// Delete record (soft delete)func (a *App) DeleteProduct(id uint) error// Export all records as PDF (returns raw bytes)func (a *App) ExportProductsPDF() ([]byte, error)// Export all records as Excel (returns raw bytes)func (a *App) ExportProductsExcel() ([]byte, error)
Calling from React
Wails generates TypeScript bindings in frontend/wailsjs/. Import and call them as async functions:
import { GetProducts, DeleteProduct, ExportProductsPDF, ExportProductsExcel } from "../../wailsjs/go/main/App";// In a component:const result = await GetProducts(page, pageSize, searchQuery);// result = { data: Product[], total: number, page: number, pages: number }await DeleteProduct(productId);const pdfBytes = await ExportProductsPDF();// Save pdfBytes as a file using browser APIsconst excelBytes = await ExportProductsExcel();// Save excelBytes as .xlsx file
The React frontend calls GetProducts(...) — NOT fetch("/api/products"). There is no HTTP involved. The call goes directly from JavaScript to Go through the Wails bridge.
10Building into Executable
Compile the entire application — Go runtime, React UI, static assets, and SQLite — into a single native binary.
This runs wails build under the hood. The React frontend is embedded via //go:embed all:frontend/dist — no separate files need to be distributed.
Build Output
| Platform | Output Path | Size |
|---|---|---|
| Windows | build/bin/myapp.exe | ~10-15MB |
| macOS | build/bin/myapp.app | ~10-15MB |
| Linux | build/bin/myapp | ~10-15MB |
Cross-Platform Builds
Windows Installer (NSIS)
Create a Windows installer with Start Menu shortcuts and uninstallation support. Requires NSIS installed:
What Gets Embedded
11Desktop vs Web — Full Comparison
Both modes share Grit conventions (GORM models, code generation, Studio), but the runtime, communication model, and distribution are fundamentally different.
| Aspect | Web (grit new) | Desktop (grit new-desktop) |
|---|---|---|
| Backend | Gin HTTP server | Wails bindings (direct Go calls) |
| Frontend | Next.js (App Router) | Vite + React + TanStack Router |
| Database | PostgreSQL | SQLite (default, local) |
| Communication | HTTP/REST + React Query + fetch | Direct Go calls + React Query + Wails bindings |
| State management | React Query + fetch | React Query + Wails bindings |
| Auth storage | JWT + HTTP cookies | JWT + local storage |
| Project structure | Turborepo monorepo (apps/api, apps/web, apps/admin) | Single directory (root Go + frontend/) |
| Distribution | Deploy to cloud / VPS | Distribute .exe / .app / binary |
| File size | N/A (web-hosted) | ~10-15MB single binary |
| Offline support | Requires server | Fully offline — all data local |
| Code generation markers | GRIT:MODELS, GRIT:ROUTES, etc. | grit:models, grit:methods, grit:nav, etc. |
| Admin panel | Full admin panel (Next.js app) | None — uses in-app pages directly |
| File storage | S3 / R2 / MinIO (presigned uploads) | Local filesystem (no S3) |
| Background jobs | asynq + Redis | None — desktop apps use goroutines if needed |
12Code Markers — NEVER Delete
These comments are injection points for the CLI. Removing them permanently breaks grit generate and grit remove. Never remove, rename, or move them.
Desktop projects use 13 grit: markers across 6 files. The CLI injects code at these locations during grit generate resource and removes it during grit remove resource.
| File | Marker | Type | Purpose |
|---|---|---|---|
| db.go | // grit:models | line-before | Add model to AutoMigrate list |
| main.go | // grit:service-init | line-before | Initialize service with DB connection |
| main.go | /* grit:app-args */ | inline | Pass service to NewApp constructor |
| app.go | // grit:imports | line-before | Import service package |
| app.go | // grit:fields | line-before | Add service field to App struct |
| app.go | /* grit:constructor-params */ | inline | Add constructor parameter |
| app.go | /* grit:constructor-assign */ | inline | Assign service to App field |
| app.go | // grit:methods | line-before | Add 7 bound methods (CRUD + export) |
| types.go | // grit:input-types | line-before | Add Input struct for resource |
| cmd/studio/main.go | // grit:studio-models | line-before | Register model in GORM Studio |
| sidebar.tsx | // grit:nav-icons + // grit:nav | line-before | Add navigation icon import + sidebar link |
What line-before vs inline Means
/* grit:marker */ comment on the SAME line. Used for constructor parameters and assignment expressions.Example: What Injection Looks Like
// BEFORE generation:type App struct {ctx context.ContextauthService *service.AuthService// grit:fields}// AFTER generation:type App struct {ctx context.ContextauthService *service.AuthServiceproductService *service.ProductService// grit:fields}
13All 12 Injection Points — What Gets Injected
For a resource named Product, here is exactly what the CLI injects at each marker:
&models.Product{},// grit:models
productService := &service.ProductService{DB: database}// grit:service-init
app := NewApp(authService, productService, /* grit:app-args */)
type App struct {ctx context.ContextauthService *service.AuthServiceproductService *service.ProductService// grit:fields}func NewApp(authService *service.AuthService, productService *service.ProductService, /* grit:constructor-params */) *App {return &App{authService: authService, productService: productService, /* grit:constructor-assign */}}
import { createFileRoute } from "@tanstack/react-router";// ... component importsexport const Route = createFileRoute("/_layout/products/")({component: ProductsListPage,});function ProductsListPage() {// ... list page with DataTable}
import { Package } from "lucide-react";// grit:nav-icons{ name: "Products", href: "/products", icon: Package },// grit:nav
14LLM Examples — Copy and Adapt
Concrete examples an AI assistant can learn from. These cover the most common workflows.
Example 1: Scaffold and Generate Multiple Resources
Create an inventory management desktop app with products and suppliers.
# Create the projectgrit new-desktop inventory# Navigate into projectcd inventory# Generate resourcesgrit generate resource Product --fields "name:string,sku:string,price:float,stock:int,reorder_level:int,active:bool"grit generate resource Supplier --fields "name:string,email:string,phone:string,address:text,notes:text"# Start developmentwails dev
Example 2: Add a Custom Service Method
Add a GetBySKU method to the product service for barcode-based lookup.
func (s *ProductService) GetBySKU(sku string) (*models.Product, error) {var product models.Productif err := s.DB.Where("sku = ?", sku).First(&product).Error; err != nil {return nil, fmt.Errorf("product not found with SKU %s: %w", sku, err)}return &product, nil}
Example 3: Expose a Custom Method via Wails
Make the custom method available in React by adding it to the App struct. Then call from React.
// Custom method — add ABOVE the // grit:methods marker (not between generated code)func (a *App) GetProductBySKU(sku string) (*models.Product, error) {return a.productService.GetBySKU(sku)}
import { GetProductBySKU } from "../../wailsjs/go/main/App";export default function ProductLookup() {const [sku, setSku] = useState("");const [product, setProduct] = useState(null);const handleLookup = async () => {try {const result = await GetProductBySKU(sku);setProduct(result);} catch (err) {toast.error("Product not found");}};return (<div><input value={sku} onChange={(e) => setSku(e.target.value)} placeholder="Scan barcode..." /><button onClick={handleLookup}>Lookup</button>{product && <div>{product.name} — {product.price}</div>}</div>);}
After adding a new Go method, restart wails dev so Wails regenerates the TypeScript bindings. Then import the new function from wailsjs/go/main/App.
Example 4: Generate with Slug Field
Generate an Article resource with an auto-generated slug from the title field.
The slug field is excluded from the form — it is auto-generated in the Go model's BeforeCreate hook. The model gets a uniqueIndex on the slug column.
Example 5: Build for Distribution
# Build for current platformgrit compile# Output:# Windows: build/bin/inventory.exe# macOS: build/bin/inventory.app# Linux: build/bin/inventory# Cross-compile for a specific platform:wails build -platform windows/amd64wails build -platform darwin/arm64# Create a Windows installer:wails build -nsis
Example 6: Remove a Resource
Cleanly remove a resource — deletes all generated files and reverses all 10 injections. Markers stay intact for future generation.
15Golden Rules for AI Assistants — Never Break These
Non-negotiable. Violating them causes silent failures, broken code generation, or corrupted project state.
Always use grit generate resource for new resources
Never create model, service, or page files manually. The CLI handles 5 file creations + 10 injections in one atomic operation. Manual creation will miss injection points and break future generation/removal.
Never modify grit: markers
The comments // grit:models, // grit:fields, // grit:methods, // grit:nav, etc. are permanent injection points. Do not remove, rename, reformat, or move them. They must stay exactly as scaffolded.
Use grit remove resource to undo — never delete files manually
Manual deletion leaves injected code orphaned in app.go, main.go, db.go, and sidebar.tsx. Always use grit remove resource <Name> which cleans up all 10 injection points.
Desktop uses SQLite, not PostgreSQL
Never suggest PostgreSQL-specific features (arrays, JSONB, full-text search) for desktop projects. SQLite is the default and only supported database. No Docker needed.
Methods must be on the App struct to be exposed to React
Only exported methods on the App struct (in app.go) are available as Wails bindings. Methods on other structs or in other files are not accessible from the frontend.
The frontend calls Go functions directly — there is no REST API
Never suggest fetch(), Axios, HTTP endpoints, or REST patterns for desktop projects. React calls Go via GetProducts(), CreateProduct(), etc. — generated Wails TypeScript bindings.
Restart wails dev after Go file changes
Wails auto-detects Go changes and rebuilds, but new methods require a full restart so TypeScript bindings are regenerated. If a new method is not available in React, restart wails dev.
Add custom methods ABOVE the grit:methods marker, not below
Code injected by grit generate goes directly above the // grit:methods marker. Place custom methods above the generated block to avoid them being accidentally removed by grit remove.
Do not create handlers or routes files for desktop
Desktop projects do not have handlers/, routes/, or middleware/ directories. All request handling is done through methods on the App struct. If a user asks for an API endpoint, explain that desktop uses Wails bindings instead.
Generate parent models before child models (belongs_to)
When using belongs_to relationships, generate the parent resource first so the referenced model exists. For example, generate Category before generating Product with a category_id:belongs_to field.
16Quick Build Reference
Copy-paste recipes for the most common desktop workflows.
grit new-desktop myappcd myappwails dev
grit generate resource Product --fields "name:string,price:float,stock:int,active:bool"grit generate resource Category --fields "name:string,description:text"grit generate resource Order --fields "customer_name:string,total:float,status:string,notes:text,completed:bool"
grit remove resource Order
grit studio# Opens browser at http://localhost:8080/studio
grit compile# Binary at: build/bin/myapp.exe (Windows) or build/bin/myapp (macOS/Linux)
wails build -platform windows/amd64wails build -platform darwin/arm64wails build -nsis # Windows installer
17Common Mistakes LLMs Make with Desktop Projects
These are the most frequent errors AI assistants make when helping with Grit Desktop. Avoid all of them.
| Wrong | Correct |
|---|---|
| Suggesting fetch("/api/products") in React | Use GetProducts() from Wails bindings |
| Creating a handlers/ or routes/ directory | Add methods to the App struct in app.go |
| Suggesting Docker or PostgreSQL setup | SQLite works out of the box — no Docker needed |
| Running grit start server + grit start client | Use grit start or wails dev (single command) |
| Manually creating model/service/page files | Use grit generate resource for everything |
| Deleting generated files to remove a resource | Use grit remove resource <Name> |
| Suggesting middleware for authentication | Auth is handled in Go methods — check JWT in service layer |
| Adding icon + nav item to sidebar.tsx | Add imports ABOVE the marker — inject goes before |
| Using pnpm dev or turbo dev for desktop | Use wails dev — no Turborepo in desktop projects |
| Creating apps/ or packages/ directories | Desktop is a single directory — no monorepo structure |