Warning
This project is experimental. Use it at your own risk.
Rails-style Go project generator. Generates hexagonal architecture projects with a single command.
go install github.com/esrid/gogen@latestRequires Go 1.26+.
| Command | Alias | Description |
|---|---|---|
gogen new |
Create a new project | |
gogen generate migration |
gogen g migration |
Add a migration file |
gogen generate auth |
gogen g auth |
Add auth to an existing project |
gogen generate oauth |
gogen g oauth |
Add OAuth2 social login providers |
gogen generate scaffold |
gogen g s |
Generate full CRUD for a model |
gogen generate attribute |
gogen g a |
Add fields to an existing scaffold |
gogen generate api |
gogen g api |
Add JSON API handler to an SSR scaffold |
gogen generate controller |
gogen g controller |
Generate a simple page/API controller |
gogen destroy scaffold |
gogen d s |
Remove a generated scaffold |
gogen destroy controller |
gogen d controller |
Remove a generated controller |
Create a new project. Prompts for anything not supplied via flags.
gogen new <project-name> [flags]Flags
| Flag | Short | Description |
|---|---|---|
--module |
-m |
Go module path (e.g. github.com/you/myapp) |
--db |
-d |
Database: sqlite or postgres |
--render |
-r |
Render mode: ssr, api, or both |
--auth |
Include authentication | |
--no-auth |
Skip auth (skip the prompt) | |
--force / --dry-run / --skip |
File conflict behaviour |
Examples
# Interactive — prompts for everything
gogen new myapp
# Fully non-interactive
gogen new myapp -m github.com/you/myapp -d sqlite -r ssr --auth
gogen new myapi -m github.com/you/myapi -d postgres -r api --no-authWhat gets generated
myapp/
├── main.go # bootstrap.Run()
├── go.mod # go 1.26
├── .env
├── .air.toml
├── Makefile
├── Dockerfile
├── docker-compose.yml
├── .gogen.yaml # project metadata for generate commands
├── bootstrap/
│ ├── app.go # Run() — DB init + server start
│ ├── config.go # env-based config (autoloads .env)
│ ├── server.go # graceful shutdown
│ ├── router.go # chi router + middleware (auto-updated)
│ └── wire_gen.go # Handlers struct + WireHandlers (auto-updated)
├── internal/
│ ├── domain/
│ │ ├── errors.go # ErrNotFound, ErrConflict, ErrUnauthorized, etc.
│ │ ├── session_port.go # SessionStore, SessionService interfaces
│ │ ├── user.go # User, NewUser, Validate(), context helpers (with --auth)
│ │ ├── auth_port.go # UserStore, UserService interfaces (with --auth)
│ │ └── email_port.go # EmailProvider interface (with --auth)
│ ├── application/
│ │ ├── auth_service.go # login/signup/reset logic (with --auth)
│ │ └── session_service.go # in-memory session cache (with --auth)
│ ├── utils/
│ │ ├── http_utils.go # WriteJSON, DecodeJSON, cookies
│ │ └── validation.go # HashPassword, PreHashing (with --auth)
│ └── adapters/
│ ├── api/
│ │ ├── middleware.go # SecurityHeaders, LimitRequestBody, NoCache
│ │ ├── middleware_auth.go # RequireAuth (with --auth)
│ │ ├── errors.go # writeError — maps domain errors to HTTP status codes
│ │ └── auth_handler.go # login/signup/reset routes (with --auth)
│ ├── web/ # SSR only
│ │ └── renderer.go # web.Render, web.RenderError — templ renderer
│ ├── db/
│ │ ├── store.go # DB connection + pool
│ │ ├── migrations.go # goose embed runner
│ │ ├── auth_store.go # user/session queries (with --auth)
│ │ └── migrations/
│ │ └── 00001_init.sql
│ └── external/email/
│ └── noop.go # email provider stub (with --auth)
└── web/ # SSR only
├── static.go
├── static/robots.txt
└── components/
├── components.templ # shared components (nav, etc.)
├── landing.templ
├── dashboard.templ # (with --auth)
└── auth/ # (with --auth)
├── login.templ
├── signup.templ
├── forgot_password.templ
├── reset_password.templ
└── settings.templ
Stack
| Concern | Library |
|---|---|
| Router | chi |
| SQLite | modernc.org/sqlite (pure Go, no CGO) |
| Postgres | pgx/v5 |
| Migrations | goose v3 (embedded SQL) |
| Templates | templ — type-safe Go SSR components |
| Env vars | godotenv/autoload — loads .env at startup |
| Password | bcrypt with sha256 pre-hashing |
Docker
Standard 2-stage build using golang:1.26-alpine:
Stage 1 — builder go build (CGO_ENABLED=0)
Stage 2 — runtime alpine:3.21 + ca-certificates + tzdata
Both SQLite (modernc.org/sqlite) and Postgres (pgx/v5) are pure Go — no CGO needed.
Create a numbered migration file in internal/adapters/db/migrations/. Reads .gogen.yaml for the DB dialect.
gogen g migration <name> [field:type ...]Smart name parsing
When the migration name follows a Rails-style convention and field args are provided, gogen generates the SQL automatically.
| Pattern | Args | Generated SQL |
|---|---|---|
AddXxxToTable |
field:type ... |
ALTER TABLE table ADD COLUMN ... |
RemoveXxxFromTable |
field:type ... |
ALTER TABLE table DROP COLUMN ... |
RenameOldToNewInTable |
(none) | ALTER TABLE table RENAME COLUMN old TO new |
Examples
# Empty skeleton
gogen g migration add_avatar_to_users
# → 00002_add_avatar_to_users.sql (empty Up/Down)
# Add columns
gogen g migration AddStatusToOrders status:string
# → ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT '';
# Remove columns
gogen g migration RemoveStatusFromOrders status:string
# → ALTER TABLE orders DROP COLUMN status;
# Rename a column
gogen g migration RenamePartNumberToSkuInProducts
# → ALTER TABLE products RENAME COLUMN part_number TO sku;Down migrations are generated automatically (reverse of Up). Reference fields also create/drop the index in the Down block.
Add authentication to a project that was created without it.
gogen g authMust be run from inside a gogen project with auth: false in .gogen.yaml.
What it does
- Creates all auth files (domain, application, utils, handler, store, email stub)
- Regenerates
main.go,bootstrap/router.go, andbootstrap/wire_gen.goto wire auth in - Re-wires all existing scaffolds in
wire_gen.goandrouter.go - Creates a new migration (
NNNNN_add_auth.sql) with the auth tables - Adds SSR auth templ components if the project uses SSR
- Adds
Userto.gogen.yamlscaffolds sogogen g attribute Userworks - Updates
.gogen.yamltoauth: true
Auth tables created
users— email, password_hash, full_name, avatar_url, timezone, soft deletesessions— token-based, 30-day expirypassword_reset_tokens— single-use, expiringpassword_reset_attempts— rate limiting (3 per hour per email)
Auth routes
| Method | Path | Description |
|---|---|---|
GET |
/auth/login |
Login page (SSR) |
POST |
/auth/login |
Authenticate |
GET |
/auth/signup |
Signup page (SSR) |
POST |
/auth/signup |
Register |
POST |
/auth/logout |
Clear session |
POST |
/auth/forgot-password |
Request reset link |
POST |
/auth/reset-password |
Reset with token |
GET |
/auth/settings |
Settings page (authenticated) |
POST |
/auth/settings/password |
Change password (authenticated) |
POST |
/auth/settings/delete |
Delete account (authenticated) |
Adding fields to the User model
Use gogen g attribute User to add columns to the users table. Regenerates internal/domain/user.go and creates the migration. The auth store, service, and handler are left untouched.
gogen g attribute User role:string plan:string
# → NNNNN_add_role_plan_to_users.sql
# → internal/domain/user.go updated with Role, Plan fieldsAfter running, update internal/adapters/db/auth_store.go to SELECT/INSERT the new columns.
Add OAuth2 social login to a project that already has auth enabled. Supports Google, Apple, and Microsoft.
gogen g oauth <provider...>Requires auth: true in .gogen.yaml. Can be run multiple times to add more providers.
Supported providers
| Provider | Env vars required |
|---|---|
google |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET |
microsoft |
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET |
apple |
APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY |
Also requires APP_URL (e.g. https://yourapp.com) for callback URLs.
Examples
gogen g oauth google
gogen g oauth google apple microsoftWhat it generates (first run)
internal/adapters/db/migrations/NNNNN_add_oauth_to_users.sql # adds provider + provider_id columns
internal/domain/oauth_port.go # OAuthStore interface
internal/adapters/db/oauth_store.go # UpsertOAuthUser implementation
internal/adapters/api/oauth_handler.go # OAuth routes
On subsequent runs (adding more providers), only oauth_handler.go is regenerated to register the new provider.
OAuth routes
| Method | Path | Description |
|---|---|---|
GET |
/auth/oauth/{provider} |
Redirect to provider |
GET |
/auth/oauth/{provider}/callback |
Handle callback, create session |
Account linking
On callback, UpsertOAuthUser resolves the user in order:
- Find by
(provider, provider_id)— returning user - Find by email — links the OAuth identity to the existing account
- Neither — creates a new user
Install deps in your project
go get github.com/markbates/goth github.com/gorilla/sessions.gogen.yaml tracking
oauth:
providers: [google, apple]Generate a full CRUD resource: migration, domain, port, store, service, and HTTP handler.
gogen g scaffold <ModelName> [field:type ...] [--protected]Must be run from inside a gogen project (reads .gogen.yaml). Auto-updates bootstrap/router.go and bootstrap/wire_gen.go.
Field types
| Type | Go type | SQLite | Postgres |
|---|---|---|---|
string / text |
string |
TEXT NOT NULL DEFAULT '' |
TEXT NOT NULL DEFAULT '' |
int |
int |
INTEGER NOT NULL DEFAULT 0 |
INTEGER NOT NULL DEFAULT 0 |
bool |
bool |
INTEGER NOT NULL DEFAULT 0 |
BOOLEAN NOT NULL DEFAULT false |
float |
float64 |
REAL NOT NULL DEFAULT 0 |
NUMERIC NOT NULL DEFAULT 0 |
time |
time.Time |
DATETIME |
TIMESTAMPTZ |
uuid |
string |
TEXT NOT NULL DEFAULT '' |
UUID NOT NULL DEFAULT gen_random_uuid() |
references |
string |
TEXT NOT NULL REFERENCES {table}(id) ON DELETE CASCADE |
UUID NOT NULL REFERENCES {table}(id) ON DELETE CASCADE |
user:references |
string |
TEXT REFERENCES users(id) ON DELETE SET NULL |
UUID REFERENCES users(id) ON DELETE SET NULL |
references is convention-based: post:references → post_id column → FK to posts(id). Table name is auto-pluralized (category → categories).
user:references (the literal form, column user_id) is generated as nullable. On non-protected routes, anonymous callers produce a record with user_id = NULL; authenticated callers get their ID injected automatically. On --protected routes, RequireAuth ensures a user is always present so NULL never occurs in practice.
When you need two FK columns pointing to the same table, use the aliased form alias:model:references:
# Two FKs to users: user_id and manager_id both reference users(id)
gogen g scaffold Employee user:references manager:user:references
# Two FKs to words: word_id and translate_id both reference words(id)
gogen g scaffold WordAssociation word:references translate:word:referencesmanager:user:references→ columnmanager_id, FK tousers(id), route/by-manager/{managerID}translate:word:references→ columntranslate_id, FK towords(id), route/by-translate/{translateID}
Aliased refs to users are treated as regular (non-auth-scoped) refs. Only the literal user:references (column user_id) triggers auth scoping with --protected.
Go field names follow standard acronym rules: user_id → UserID, avatar_url → AvatarURL.
Example
gogen g scaffold Post title:string body:text user:references published:boolGenerated files
internal/domain/post.go
internal/domain/post_port.go
internal/application/post_service.go
internal/adapters/db/post_store.go
internal/adapters/api/post_handler.go
internal/adapters/db/migrations/NNNNN_create_posts.sql
With --render ssr or --render both, a templ component folder is also created:
web/components/posts/
├── index.templ
├── show.templ
├── new.templ
└── edit.templ
With --render both, an SSR web handler and API handler are both generated:
internal/adapters/web/post_handler.go # SSR handler (GET /posts → HTML)
internal/adapters/api/post_handler.go # API handler (GET /api/posts → JSON)
Validation
Generated domain structs include a Validate() method that checks required string/reference fields. The service layer calls it automatically on create and update — no validation logic leaks into handlers or utils.
// internal/domain/post.go
func (m Post) Validate() error {
if strings.TrimSpace(m.Title) == "" {
return fmt.Errorf("%w: title is required", ErrInvalidInput)
}
return nil
}HTTP endpoints
| Method | Path | Handler |
|---|---|---|
GET |
/posts |
list all |
POST |
/posts |
create |
GET |
/posts/{id} |
get one |
PUT / POST |
/posts/{id} |
update (PUT for API, POST for SSR forms) |
DELETE / POST |
/posts/{id}/delete |
delete (DELETE for API, POST for SSR forms) |
Association queries
When a references field is present, gogen generates filtered query methods at every layer.
For non-user refs (e.g. post:references), a list-by route is also exposed:
| Layer | Method |
|---|---|
| Store | ListCommentsByPostID(ctx, postID) |
| Service | ListByPostID(ctx, postID) |
| Handler | GET /comments/by-post/{postID} |
user:references is treated specially — no public list-by route is generated. Instead, --protected + user:references scopes the default list endpoint to the current user automatically.
Multiple refs each get their own method and route:
gogen g scaffold Comment body:text post:references category:references
# GET /comments/by-post/{postID}
# GET /comments/by-category/{categoryID}--protected flag
Requires auth: true in .gogen.yaml. Mounts the scaffold routes inside the RequireAuth middleware group.
gogen g scaffold Post title:string body:text user:references --protectedThree things happen automatically:
- Routes are mounted inside the protected group (behind
RequireAuth) createinjects the current user's ID into theuser_idfield (whenuser:referencesis present)listusesservice.ListByUserID(ctx, userID)instead ofservice.List(ctx)— scoped to current user
Auto-wiring
After generation, bootstrap/wire_gen.go and bootstrap/router.go are updated automatically — no manual edits needed:
// bootstrap/wire_gen.go (auto-generated)
type Handlers struct {
Post *api.PostHandler
}
func WireHandlers(dbStore *db.Store, logger *slog.Logger) *Handlers {
h := &Handlers{}
postSvc := application.NewPostService(dbStore)
h.Post = api.NewPostHandler(postSvc, logger)
return h
}Add new fields to an existing scaffold. Updates the domain, store, and handler; creates an ALTER TABLE migration; and regenerates SSR templ components if applicable.
gogen g attribute <ModelName> field:type [field:type ...]Must be run from inside a gogen project. The model must already exist (created via gogen g scaffold or gogen g auth).
Example
gogen g attribute Post published:bool views:intWhat it does:
- Creates
NNNNN_add_published_views_to_posts.sqlwithALTER TABLEstatements - Regenerates
post.go,post_store.go,post_handler.gowith the new fields - Regenerates SSR templ components (
web/components/posts/*.templ) only when--viewsis passed - Updates
.gogen.yamlwith the new field list
Accepts the same field types as gogen g scaffold. Duplicate fields are rejected.
SSR views are not regenerated by default to preserve any customisations you've made. Pass
--viewsto overwrite them.
Auth User model
gogen g attribute User is supported after gogen g auth. It regenerates internal/domain/user.go using the auth-aware template (preserving all auth logic) and creates the migration. The auth store, service, and handler are not touched — update auth_store.go manually to SELECT/INSERT the new columns.
gogen g attribute User role:string
# → NNNNN_add_role_to_users.sql
# → internal/domain/user.go (Role string field added to User struct)
# → hint: update auth_store.go SELECT/INSERTAdd a JSON API handler to an existing SSR scaffold. Useful when you want to expose a REST API alongside your server-rendered pages.
gogen g api <ModelName>Must be run from inside a gogen project with render: ssr. The scaffold must already exist.
Example
gogen g api Post
# generates: internal/adapters/api/post_api_handler.go
# routes: GET /api/posts, POST /api/posts, GET /api/posts/{id}, etc.Updates .gogen.yaml and rewires bootstrap/wire_gen.go and bootstrap/router.go automatically.
Generate a simple page or API controller with no model, store, or service — useful for static-ish pages like contact, about, terms, etc.
gogen g controller <Name> [--protected] [--route /path]Flags
| Flag | Description |
|---|---|
--protected |
Mount behind RequireAuth middleware |
--route |
Custom route path (default: /<name>) |
Examples
gogen g controller Contact
# GET /contact → web/components/contact/page.templ
gogen g controller Dashboard --protected --route /dashboard
# GET /dashboard → protected, web/components/dashboard/page.templGenerated files (SSR)
internal/adapters/web/contact_handler.go
web/components/contact/page.templ
Generated files (API)
internal/adapters/api/contact_handler.go
Auto-wired into bootstrap/wire_gen.go and bootstrap/router.go.
Remove all files generated by gogen g scaffold. Updates bootstrap/wire_gen.go and bootstrap/router.go automatically.
gogen d scaffold <ModelName>Example
gogen d scaffold PostRemoves:
internal/domain/post.go
internal/domain/post_port.go
internal/application/post_service.go
internal/adapters/db/post_store.go
internal/adapters/api/post_handler.go
internal/adapters/api/post_api_handler.go # both/api mode
internal/adapters/web/post_handler.go # ssr/both mode
web/components/posts/ # SSR only
internal/adapters/db/migrations/*_create_posts.sql
Migration warning
If a matching migration file is found it is deleted, but a warning is printed:
warning migration 00003_create_posts.sql was deleted — run goose down manually if already applied
If you already ran goose up against a real database, run the down migration first:
goose -dir internal/adapters/db/migrations sqlite3 myapp.db down
gogen d scaffold Post--dry-run prints what would be removed without deleting anything.
Remove all files generated by gogen g controller. Updates bootstrap/wire_gen.go and bootstrap/router.go automatically.
gogen d controller <Name>Example
gogen d controller ContactRemoves:
internal/adapters/web/contact_handler.go # SSR
internal/adapters/api/contact_handler.go # API
web/components/contact/ # SSR
Generated projects use templ — a type-safe Go templating language that compiles to plain Go functions.
Define a component (web/components/card/card.templ):
package card
templ Card(title, body string) {
<div class="card">
<h2>{ title }</h2>
<p>{ body }</p>
</div>
}Use it in a page (web/components/posts/index.templ):
package posts
import "myapp/web/layouts"
import "myapp/web/components/card"
templ Index(posts []domain.Post) {
@layouts.Layout("Posts") {
for _, p := range posts {
@card.Card(p.Title, p.Body)
}
}
}Render from a handler:
web.Render(w, r, posts.Index(items))Error rendering:
web.RenderError(w, r, err) // maps domain errors to HTTP status + error pageweb.RenderError maps domain errors to the correct HTTP status automatically:
| Domain error | HTTP status |
|---|---|
ErrNotFound |
404 |
ErrUnauthorized |
401 |
ErrForbidden |
403 |
ErrConflict |
409 |
| anything else | 500 |
Domain errors are defined in internal/domain/errors.go and mapped to HTTP status codes only at the adapter layer — never inside domain or application code.
API adapter (internal/adapters/api/errors.go):
writeError(w, err) // maps domain errors to JSON error responsesSSR adapter (internal/adapters/web/renderer.go):
web.RenderError(w, r, err) // maps domain errors to HTML error pagesAvailable on all commands:
| Flag | Short | Description |
|---|---|---|
--force |
-f |
Overwrite existing files |
--dry-run |
-p |
Preview without writing |
--skip |
-s |
Skip existing files silently |