Getting Started

Configuration

A complete reference for every environment variable in your Grit project. All configuration is done through the .env file at the project root.

.envTyped ConfigWired intoparse.envkey = valueconfig.Configparsed + typedDatabaseDATABASE_URLRedisREDIS_URLStorageS3_*EmailRESEND_*
One .env fileTyped config struct
Every setting lives in .env, parsed once into a typed Config that wires each service

Environment Files

Every Grit project includes three environment files:

  • .env -- Your actual configuration, generated at scaffold time with strong random secrets (JWT, Postgres, Pulse, Sentinel). This file is gitignored and never committed.
  • .env.example -- Documented template with all variables and sensible defaults. Committed to git.
  • .env.cloud.example -- Template for cloud-only setups (Neon, Upstash, R2/B2) when you do not have Docker.

The Go API loads these variables at startup using godotenv and parses them into a typed Config struct. Environment variables are read once at startup and are available throughout the application.

Application

General application settings that control the server behavior.

.env
# App — General application settings
APP_NAME=myapp # Application name (used in emails, logs)
APP_ENV=development # Environment: development, staging, production
APP_PORT=8080 # API server port
APP_URL=http://localhost:8080
APP_NAMEProject name

Used as the application title in email templates, log entries, and the admin panel header. Set to your project name during scaffolding.

APP_ENVdevelopment

Controls logging verbosity, GORM Studio visibility, and error detail level. Set to production in deployed environments to disable debug features. Note: Pulse and Sentinel refuse to mount in production if their passwords are still the literal defaults.

APP_PORT8080

The port the Go API server listens on. The frontend apps proxy API requests to this port.

APP_URLhttp://localhost:8080

The full URL of the API server. Used for generating absolute URLs in emails and file storage signed URLs.

Database

Grit uses PostgreSQL as its primary database, connected through GORM. Rather than a single connection string, the scaffold treats the POSTGRES_* parts as the single source of truth -- both docker-compose.yml and the Go API read them. The Go API builds the connection DSN from these parts automatically when DATABASE_URL is left empty.

.env
# Database (Postgres) — single source of truth
# Edit ONLY the POSTGRES_* values below. docker-compose.yml reads them via
# ${VAR} substitution and the Go API builds the DSN from these parts when
# DATABASE_URL is empty.
POSTGRES_USER=grit
POSTGRES_PASSWORD=change-me # generated per-scaffold; MUST change in production
POSTGRES_DB=myapp
POSTGRES_HOST=localhost # "postgres" inside docker-compose.prod.yml
POSTGRES_PORT=5434 # host port; 5432 inside the docker network
# Override the connection string ONLY for external Postgres (Neon, Supabase,
# RDS) or SQLite. When set, this wins over the POSTGRES_* parts above.
# DATABASE_URL=postgres://user:pass@host:5432/db?sslmode=require
# DATABASE_URL=sqlite:./app.db # pure-Go driver, no CGO
# DATABASE_URL=sqlite::memory: # gone on restart, great for tests
# DATABASE_URL=
POSTGRES_USERgrit

Postgres role the API connects as. Matches the user created by docker-compose.yml.

POSTGRES_PASSWORD(generated)

Password for the Postgres role. grit new generates a strong random value per project so a fresh scaffold runs without editing. Change it in production.

POSTGRES_DBProject name

Name of the database to connect to. Defaults to your project name.

POSTGRES_HOSTlocalhost

Database host. Stays localhost for local Docker; docker-compose.prod.yml overrides it to "postgres" (the service name) for inter-container traffic.

POSTGRES_PORT5434

Host port for Postgres. Grit uses 5434 (not the default 5432) to avoid collisions with a Postgres you may already run locally. The container still listens on 5432 inside the Docker network.

DATABASE_URLoptional

Optional full connection string. Leave commented to build the DSN from the POSTGRES_* parts. Set it to point at an external Postgres (postgres://... with sslmode=require) or to use SQLite (sqlite:./app.db or sqlite::memory:). When set, it wins over the POSTGRES_* parts.

JWT Authentication

Grit uses JWT tokens for authentication with separate access and refresh tokens. The access token is short-lived (15 minutes) and the refresh token lasts 7 days.JWT_SECRET is generated at scaffold time.

.env
# JWT — generated at scaffold time. Rotate with: openssl rand -hex 32
JWT_SECRET=change-me-in-production
JWT_ACCESS_EXPIRY=15m # Access token lifetime
JWT_REFRESH_EXPIRY=168h # Refresh token lifetime (7 days)
JWT_SECRET(generated)

The secret key used to sign and verify JWT tokens. Generated per-scaffold (openssl rand -hex 32). Rotate it in production. Both access and refresh tokens use this same secret.

JWT_ACCESS_EXPIRY15m

How long access tokens are valid. Uses Go duration format: 15m (15 minutes), 1h (1 hour), etc. Keep short for security. The frontend automatically refreshes expired tokens.

JWT_REFRESH_EXPIRY168h

How long refresh tokens are valid. 168h is 7 days. Users must re-login after this period.

OAuth & Social Login

Grit ships with Google and GitHub OAuth2 sign-in. The OAuth API routes are always registered server-side; SOCIAL_AUTH_ENABLED only controls whether the social buttons render on the auth pages. Leave the client IDs and secrets empty to run without social login.

.env
# OAuth2 — Social Login (Google + GitHub)
# Google: https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID= # Google OAuth 2.0 Client ID
GOOGLE_CLIENT_SECRET= # Google OAuth 2.0 Client Secret
# GitHub: https://github.com/settings/developers
GITHUB_CLIENT_ID= # GitHub OAuth App Client ID
GITHUB_CLIENT_SECRET= # GitHub OAuth App Client Secret
OAUTH_FRONTEND_URL=http://localhost:3001 # Where to redirect after OAuth
# Toggle the social buttons + "or continue with" divider on auth pages.
# Mirrored to the browser bundle as NEXT_PUBLIC_SOCIAL_AUTH_ENABLED.
SOCIAL_AUTH_ENABLED=true
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET(your keys)

Google OAuth 2.0 credentials. Create them in the Google Cloud Console under APIs & Services > Credentials.

GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET(your keys)

GitHub OAuth App credentials. Create an OAuth App under GitHub Settings > Developers.

OAUTH_FRONTEND_URLhttp://localhost:3001

The frontend URL the API redirects back to after a successful OAuth exchange. Defaults to the admin panel dev port.

SOCIAL_AUTH_ENABLEDtrue

Show or hide the Google/GitHub buttons on auth pages. Set to false to remove them and the "or continue with" divider. The OAuth routes stay registered either way. Mirrored to the browser as NEXT_PUBLIC_SOCIAL_AUTH_ENABLED.

Redis

Redis is used for response caching and background job queues (via Asynq). A single Redis instance handles both use cases.

.env
# Redis — Cache and job queue
REDIS_URL=redis://localhost:6380
REDIS_URLredis://localhost:6380

Redis connection URL. For local Docker, Grit maps Redis to host port 6380 (not the default 6379) to avoid collisions with a local Redis. For cloud Redis (Upstash), use the rediss:// protocol (with double s) and include the password: rediss://default:password@endpoint:6379.

Frontend & Public URLs

URLs baked into the Next.js web and admin bundles at build time. These tell the clients where to reach the API and the admin panel.

.env
# Public API URL — baked into Next.js bundles at build time
API_URL=http://localhost:8080
# Admin panel URL — surfaced in the web app's navbar + landing-page dev links.
# Set to your production admin origin (e.g. https://admin.example.com) before shipping.
NEXT_PUBLIC_ADMIN_URL=http://localhost:3001
API_URLhttp://localhost:8080

The public API base URL baked into the Next.js bundles at build time. Point it at your deployed API origin (e.g. https://api.example.com) for production builds.

NEXT_PUBLIC_ADMIN_URLhttp://localhost:3001

The admin panel URL surfaced in the web app navbar and landing-page dev links. Defaults to the admin dev port; set to your production admin origin before shipping.

File Storage

Grit supports four S3-compatible storage providers: MinIO (local development), AWS S3, Cloudflare R2, and Backblaze B2. The STORAGE_DRIVER variable controls which provider is active -- only the variables for the active driver need to be set.

.env
# Storage — Which provider to use: minio, s3, r2, b2
STORAGE_DRIVER=minio
# MinIO — Local S3-compatible storage (default for development)
MINIO_ENDPOINT=http://localhost:9002
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=myapp-uploads
MINIO_REGION=us-east-1
MINIO_USE_SSL=false
# AWS S3 (used when STORAGE_DRIVER=s3)
# Leave S3_ENDPOINT empty to use the AWS regional default. S3_ACCESS_KEY /
# S3_SECRET_KEY / S3_REGION fall back to AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY
# / AWS_REGION, so an IAM role on your EC2 / ECS / Lambda Just Works.
S3_ENDPOINT=
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET=
S3_REGION=us-east-1
# Cloudflare R2 (used when STORAGE_DRIVER=r2)
R2_ENDPOINT=
R2_ACCESS_KEY= # R2 Access Key ID
R2_SECRET_KEY= # R2 Secret Access Key
R2_BUCKET=
R2_REGION=auto # Always "auto" for R2
# Backblaze B2 (used when STORAGE_DRIVER=b2)
B2_ENDPOINT=
B2_ACCESS_KEY= # B2 keyID
B2_SECRET_KEY= # B2 applicationKey
B2_BUCKET=
B2_REGION=us-west-004 # Must match your bucket region
STORAGE_DRIVERminio

Which storage provider to use. Options: minio (local dev with Docker), s3 (AWS S3), r2 (Cloudflare R2), b2 (Backblaze B2). Only the variables for the active driver need to be set.

MINIO_ENDPOINThttp://localhost:9002

MinIO server URL. Grit maps MinIO to host port 9002 (API) and 9003 (web console) to avoid the default 9000/9001 colliding with other services.

MINIO_ACCESS_KEY / MINIO_SECRET_KEYminioadmin

Default MinIO credentials. These match the Docker Compose configuration. Change in production if running your own MinIO instance.

S3_ENDPOINT(empty)

AWS S3 endpoint. Leave empty to use the AWS regional default (s3.<region>.amazonaws.com) with virtual-hosted-style addressing. Only used when STORAGE_DRIVER=s3.

S3_ACCESS_KEY / S3_SECRET_KEY / S3_REGION(IAM fallback)

AWS credentials and region. Fall back to AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION, so leaving them empty lets an attached IAM role supply the credentials automatically.

R2_ENDPOINT(your account)

Cloudflare R2 endpoint. Format: https://ACCOUNT_ID.r2.cloudflarestorage.com. Only used when STORAGE_DRIVER=r2.

R2_ACCESS_KEY / R2_SECRET_KEY(your keys)

Cloudflare R2 API credentials. Create an API token in the Cloudflare Dashboard under R2 > Manage R2 API Tokens. R2_REGION is always "auto".

B2_ENDPOINT(your region)

Backblaze B2 S3-compatible endpoint. Format depends on your bucket region. B2_ACCESS_KEY is the keyID and B2_SECRET_KEY is the applicationKey. Only used when STORAGE_DRIVER=b2.

Email

Grit uses Resend for transactional emails -- welcome emails, password resets, and notifications. In development, emails are caught by Mailhog (accessible at http://localhost:8025).

.env
# Email — Resend integration
RESEND_API_KEY=re_your_api_key
MAIL_FROM=noreply@myapp.dev
# Support inbox — every ticket opened in /system/support is emailed here
# (when RESEND_API_KEY is set). Leave empty in dev to skip email-out.
SUPPORT_EMAIL=
RESEND_API_KEYre_your_api_key

Your Resend API key. Get one at resend.com/api-keys. In development, emails are sent to Mailhog regardless of this key.

MAIL_FROMnoreply@myapp.dev

The sender email address for all outgoing emails. Must be a verified domain in your Resend account for production use.

SUPPORT_EMAIL(empty)

Destination inbox for support tickets opened in /system/support. Tickets are emailed here when RESEND_API_KEY is set. Leave empty in dev to skip sending.

CORS

Cross-Origin Resource Sharing configuration. The Go API needs to know which frontend origins are allowed to make requests. The Wails desktop webview does not need an entry -- its origin is matched by host (wails.localhost) on any port.

.env
# CORS — Allowed frontend origins (comma-separated)
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
CORS_ORIGINShttp://localhost:3000,http://localhost:3001

Comma-separated list of allowed origins. In development, port 3000 is the web app and port 3001 is the admin panel. In production, set this to your actual domain names (e.g., https://myapp.com,https://admin.myapp.com).

GORM Studio

The embedded visual database browser. Accessible at /studio on the API server, behind its own basic-auth login.

.env
# GORM Studio — Visual database browser
GORM_STUDIO_ENABLED=true
GORM_STUDIO_USERNAME=admin # Login username for the Studio UI
GORM_STUDIO_PASSWORD=studio # Login password for the Studio UI
GORM_STUDIO_ENABLEDtrue

Enable or disable GORM Studio. Set to true in development for visual database browsing. Set to false in production to disable the browser and prevent unauthorized access to your data.

GORM_STUDIO_USERNAMEadmin

Login username for the Studio UI.

GORM_STUDIO_PASSWORDstudio

Login password for the Studio UI. Change it if you keep Studio enabled outside local development.

AI Integration

Grit ships with built-in AI support via Vercel AI Gateway (one key, hundreds of models). The AI service provides text completion and streaming endpoints.

.env
# AI — Vercel AI Gateway (one key, hundreds of models)
AI_GATEWAY_API_KEY= # Get from vercel.com/ai-gateway
AI_GATEWAY_MODEL=anthropic/claude-sonnet-4-6 # provider/model format
AI_GATEWAY_URL=https://ai-gateway.vercel.sh/v1
AI_GATEWAY_API_KEY(your key)

Your Vercel AI Gateway API key. Get one at vercel.com/ai-gateway. A single key gives you access to all providers (Anthropic, OpenAI, Google, and more). Leave empty if you do not use AI features.

AI_GATEWAY_MODELanthropic/claude-sonnet-4-6

The model to use, in provider/model format. Examples: anthropic/claude-sonnet-4-6, openai/gpt-5.4, google/gemini-2.5-pro.

AI_GATEWAY_URLhttps://ai-gateway.vercel.sh/v1

The Vercel AI Gateway endpoint URL. This is the same for all providers and models. You should not need to change this.

Two-Factor Authentication

Grit supports TOTP-based two-factor authentication. The issuer name appears in authenticator apps (e.g., Google Authenticator, Authy) alongside the user's account.

.env
# Two-Factor Authentication (TOTP)
TOTP_ISSUER=myapp
TOTP_ISSUERmyapp

The issuer name displayed in authenticator apps when users set up 2FA. Set this to your application or company name.

Observability (Pulse)

Pulse is Grit's built-in performance monitoring, request-tracing, and error-tracking dashboard. Its password is generated at scaffold time -- Pulse refuses to mount in APP_ENV=production while the password is still the literal default pulse.

.env
# Observability — Pulse performance monitoring dashboard
PULSE_ENABLED=true # Set to "false" to disable Pulse entirely
PULSE_USERNAME=admin # Dashboard login username
PULSE_PASSWORD=pulse # Generated per-scaffold. Rotate: openssl rand -hex 16
PULSE_ENABLEDtrue

Turn the Pulse dashboard on or off. Set to false to disable Pulse entirely.

PULSE_USERNAMEadmin

Login username for the Pulse dashboard.

PULSE_PASSWORD(generated)

Login password for the Pulse dashboard. Generated per-scaffold so the production gate is satisfied out of the box. Rotate with openssl rand -hex 16.

Security (Sentinel)

Sentinel is Grit's built-in WAF, rate limiter, and threat-detection layer with its own dashboard. Like Pulse, it refuses to mount in production while its credentials are still the defaults. Both SENTINEL_PASSWORD andSENTINEL_SECRET_KEY are generated at scaffold time.

.env
# Security — Sentinel WAF, rate limiting, threat detection
SENTINEL_ENABLED=true # Set to "false" to disable Sentinel entirely
SENTINEL_USERNAME=admin # Dashboard login username
SENTINEL_PASSWORD=sentinel # Generated per-scaffold. Rotate: openssl rand -hex 16
SENTINEL_SECRET_KEY=change-me # Generated per-scaffold (>=32 bytes). Rotate: openssl rand -hex 32
SENTINEL_ENABLEDtrue

Turn Sentinel on or off. Set to false to disable the WAF, rate limiting, and threat detection entirely.

SENTINEL_USERNAMEadmin

Login username for the Sentinel dashboard.

SENTINEL_PASSWORD(generated)

Login password for the Sentinel dashboard. Generated per-scaffold. Rotate with openssl rand -hex 16.

SENTINEL_SECRET_KEY(generated)

Secret used to sign the Sentinel dashboard JWT sessions. Needs at least 32 bytes of entropy; generated per-scaffold. Rotate with openssl rand -hex 32.

Theme

Picks the visual identity for the auth pages and dashboard. The value is mirrored into NEXT_PUBLIC_THEME for the web and admin clients innext.config, so server components render the right theme without a flash of unstyled content.

.env
# Theme — visual identity for auth pages + dashboard
# atlas — split-screen, team/organisation, Inter (default)
# aurora — centered Clerk-style, consumer SaaS, Geist
# pulse — split-screen with carousel, ecommerce/brand, Onest + DM Serif
THEME=atlas
THEMEatlas

Chosen with --theme at scaffold time (defaults to atlas). Options: atlas (split-screen, team/organisation, Inter), aurora (centered Clerk-style, consumer SaaS, Geist), and pulse (split-screen with carousel, ecommerce/brand, Onest + DM Serif). Mirrored to the clients as NEXT_PUBLIC_THEME.

Local Service Ports

The services started by Docker Compose and the dev servers each bind their own host port. Grit shifts the infrastructure ports off their defaults (Postgres, Redis, MinIO) to avoid colliding with services you may already run locally.

ServiceDev URLHost Port
Go APIhttp://localhost:80808080
GORM Studiohttp://localhost:8080/studio8080
Web App (Next.js)http://localhost:30003000
Admin Panel (Next.js)http://localhost:30013001
PostgreSQLlocalhost:54345434
Redislocalhost:63806380
MinIO (API)http://localhost:90029002
MinIO Consolehttp://localhost:90039003
Mailhoghttp://localhost:80258025

Complete .env Reference

Here is every environment variable in a single block for quick copy-paste:

.env.example
# App
APP_NAME=myapp
APP_ENV=development
APP_PORT=8080
APP_URL=http://localhost:8080
# Database (Postgres) — edit these; DATABASE_URL is optional
POSTGRES_USER=grit
POSTGRES_PASSWORD=change-me
POSTGRES_DB=myapp
POSTGRES_HOST=localhost
POSTGRES_PORT=5434
# DATABASE_URL=postgres://user:pass@host:5432/db?sslmode=require
# DATABASE_URL=sqlite:./app.db
# DATABASE_URL=sqlite::memory:
# JWT
JWT_SECRET=change-me-in-production
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=168h
# OAuth2 — Social Login (Google + GitHub)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
OAUTH_FRONTEND_URL=http://localhost:3001
SOCIAL_AUTH_ENABLED=true
# Redis
REDIS_URL=redis://localhost:6380
# Frontend & Public URLs
API_URL=http://localhost:8080
NEXT_PUBLIC_ADMIN_URL=http://localhost:3001
# Storage — minio, s3, r2, b2
STORAGE_DRIVER=minio
# MinIO (local dev)
MINIO_ENDPOINT=http://localhost:9002
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=myapp-uploads
MINIO_REGION=us-east-1
MINIO_USE_SSL=false
# AWS S3
S3_ENDPOINT=
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET=
S3_REGION=us-east-1
# Cloudflare R2
R2_ENDPOINT=
R2_ACCESS_KEY=
R2_SECRET_KEY=
R2_BUCKET=
R2_REGION=auto
# Backblaze B2
B2_ENDPOINT=
B2_ACCESS_KEY=
B2_SECRET_KEY=
B2_BUCKET=
B2_REGION=us-west-004
# Email
RESEND_API_KEY=re_your_api_key
MAIL_FROM=noreply@myapp.dev
SUPPORT_EMAIL=
# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
# GORM Studio
GORM_STUDIO_ENABLED=true
GORM_STUDIO_USERNAME=admin
GORM_STUDIO_PASSWORD=studio
# AI (Vercel AI Gateway)
AI_GATEWAY_API_KEY=
AI_GATEWAY_MODEL=anthropic/claude-sonnet-4-6
AI_GATEWAY_URL=https://ai-gateway.vercel.sh/v1
# Two-Factor Authentication (TOTP)
TOTP_ISSUER=myapp
# Observability (Pulse)
PULSE_ENABLED=true
PULSE_USERNAME=admin
PULSE_PASSWORD=pulse
# Security (Sentinel)
SENTINEL_ENABLED=true
SENTINEL_USERNAME=admin
SENTINEL_PASSWORD=sentinel
SENTINEL_SECRET_KEY=change-me
# Theme — atlas, aurora, pulse
THEME=atlas

Production Checklist

Before deploying to production, make sure you have addressed these configuration items:

  • Change JWT_SECRET to a strong random string (at least 32 characters)
  • Set APP_ENV=production to disable debug logging and GORM Studio
  • Set GORM_STUDIO_ENABLED=false (or change its username/password) to lock down the database browser
  • Change POSTGRES_PASSWORD (or set DATABASE_URL to your managed Postgres with sslmode=require)
  • Update CORS_ORIGINS to your production domain names
  • Set STORAGE_DRIVER to s3, r2, or b2 and configure the cloud credentials
  • Set RESEND_API_KEY with your production API key, verify your sender domain, and set SUPPORT_EMAIL
  • Update REDIS_URL to your production Redis instance
  • Set API_URL and NEXT_PUBLIC_ADMIN_URL to your production origins
  • Rotate PULSE_PASSWORD, SENTINEL_PASSWORD, and SENTINEL_SECRET_KEY off the literal defaults (they gate mounting in production)
  • Fill in OAuth credentials if you use social login, or set SOCIAL_AUTH_ENABLED=false
  • Set APP_URL to your production API domain