@nakedev/go-scaffold 0.3.3 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +288 -50
  2. package/dist/commands/auth.js +53 -22
  3. package/dist/commands/config.js +50 -0
  4. package/dist/commands/create.js +32 -2
  5. package/dist/commands/generate.js +25 -2
  6. package/dist/commands/method.js +22 -7
  7. package/dist/commands/migration.js +2 -2
  8. package/dist/commands/observability.js +3 -3
  9. package/dist/commands/rbac.js +3 -3
  10. package/dist/commands/undo.js +5 -0
  11. package/dist/commands/worker.js +1 -1
  12. package/dist/index.js +186 -59
  13. package/dist/prompts/auth-wizard.js +40 -6
  14. package/dist/prompts/create-wizard.js +43 -2
  15. package/dist/prompts/generate-wizard.js +89 -9
  16. package/dist/templates/auth-manifest.js +31 -1
  17. package/dist/templates/create-manifest.js +4 -0
  18. package/dist/templates/module-manifest.js +37 -1
  19. package/dist/templates/rbac-manifest.js +1 -0
  20. package/dist/types.js +6 -0
  21. package/dist/utils/auth-patcher.js +115 -24
  22. package/dist/utils/config.js +147 -3
  23. package/dist/utils/main-patcher.js +29 -27
  24. package/dist/utils/marker-patch.js +7 -1
  25. package/dist/utils/method-patcher.js +261 -81
  26. package/dist/utils/module-profile.js +32 -0
  27. package/dist/utils/observability-patcher.js +2 -2
  28. package/dist/utils/platform-patcher.js +29 -7
  29. package/dist/utils/rbac-patcher.js +97 -75
  30. package/package.json +7 -2
  31. package/templates/add/auth/cmd/seed/main.go.hbs +13 -3
  32. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  33. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  34. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  35. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  36. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  37. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  38. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  39. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  40. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  41. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  42. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  43. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  44. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  45. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +113 -0
  46. package/templates/add/auth/internal/app/user/browser_policy.go.hbs +98 -0
  47. package/templates/add/auth/internal/app/user/composition.go.hbs +165 -0
  48. package/templates/add/auth/internal/app/user/contracts.go.hbs +88 -0
  49. package/templates/add/auth/internal/app/user/dto.go.hbs +57 -0
  50. package/templates/add/auth/internal/app/user/errors.go.hbs +25 -0
  51. package/templates/add/auth/internal/app/user/external_login.go.hbs +208 -0
  52. package/templates/add/auth/internal/app/user/handler.go.hbs +60 -203
  53. package/templates/add/auth/internal/app/user/handler_local.go.hbs +75 -0
  54. package/templates/add/auth/internal/app/user/handler_mfa.go.hbs +83 -0
  55. package/templates/add/auth/internal/app/user/handler_oauth.go.hbs +70 -0
  56. package/templates/add/auth/internal/app/user/handler_recovery.go.hbs +49 -0
  57. package/templates/add/auth/internal/app/user/handler_test.go.hbs +290 -0
  58. package/templates/add/auth/internal/app/user/handler_user.go.hbs +41 -0
  59. package/templates/add/auth/internal/app/user/jwt.go.hbs +6 -59
  60. package/templates/add/auth/internal/app/user/local_auth.go.hbs +98 -0
  61. package/templates/add/auth/internal/app/user/mfa_service.go.hbs +450 -0
  62. package/templates/add/auth/internal/app/user/mfa_service_test.go.hbs +199 -0
  63. package/templates/add/auth/internal/app/user/mfa_store.go.hbs +127 -0
  64. package/templates/add/auth/internal/app/user/mfa_store_test.go.hbs +174 -0
  65. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +8 -2
  66. package/templates/add/auth/internal/app/user/model/identity.go.hbs +4 -3
  67. package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +17 -0
  68. package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +20 -0
  69. package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +17 -0
  70. package/templates/add/auth/internal/app/user/model/user.go.hbs +3 -2
  71. package/templates/add/auth/internal/app/user/provider_test.go.hbs +286 -0
  72. package/templates/add/auth/internal/app/user/recovery_service.go.hbs +114 -0
  73. package/templates/add/auth/internal/app/user/repository.go.hbs +2 -0
  74. package/templates/add/auth/internal/app/user/service.go.hbs +82 -478
  75. package/templates/add/auth/internal/app/user/service_test.go.hbs +601 -45
  76. package/templates/add/auth/internal/app/user/session_cookie.go.hbs +33 -0
  77. package/templates/add/auth/internal/app/user/sessions.go.hbs +99 -0
  78. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +42 -14
  79. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +105 -40
  80. package/templates/add/auth/internal/app/user/tokenstore_pg_test.go.hbs +96 -0
  81. package/templates/add/auth/internal/app/user/tokenstore_recovery.go.hbs +58 -0
  82. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +144 -70
  83. package/templates/add/auth/internal/app/user/tokenstore_redis_test.go.hbs +185 -0
  84. package/templates/add/auth/internal/app/user/user_query.go.hbs +65 -0
  85. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  86. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  87. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +9 -4
  88. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  89. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  90. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  91. package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -2
  92. package/templates/add/rbac/internal/app/role/composition.go.hbs +35 -0
  93. package/templates/add/rbac/internal/app/role/service.go.hbs +12 -12
  94. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +340 -121
  95. package/templates/create/base/.env.example.hbs +0 -1
  96. package/templates/create/base/AGENTS.md.hbs +255 -67
  97. package/templates/create/base/Makefile.hbs +2 -1
  98. package/templates/create/base/README.md.hbs +45 -17
  99. package/templates/create/base/cmd/api/wiring.go.hbs +18 -25
  100. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  101. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  102. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  103. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  104. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  105. package/templates/create/features/docs/architecture.md.hbs +38 -16
  106. package/templates/create/features/docs/patterns.md.hbs +40 -21
  107. package/templates/create/features/docs/techstack.md.hbs +3 -3
  108. package/templates/generate/module/commands.go.hbs +95 -0
  109. package/templates/generate/module/composition.go.hbs +23 -0
  110. package/templates/generate/module/cqrs_test.go.hbs +7 -0
  111. package/templates/generate/module/handler.go.hbs +50 -5
  112. package/templates/generate/module/minimal/commands.go.hbs +34 -0
  113. package/templates/generate/module/minimal/handler.go.hbs +34 -0
  114. package/templates/generate/module/minimal/queries.go.hbs +45 -0
  115. package/templates/generate/module/minimal/service.go.hbs +27 -1
  116. package/templates/generate/module/queries.go.hbs +62 -0
  117. package/templates/generate/module/service.go.hbs +61 -5
  118. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  119. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
@@ -1,80 +1,268 @@
1
1
  # Agent Guidance: {{projectName}}
2
2
 
3
- This project was scaffolded by `@nakedev/go-scaffold` and is maintained using
4
- its `generate` subcommands new domain modules and new endpoints are not
5
- hand-written.
3
+ This project was scaffolded by `@nakedev/go-scaffold`. It is a modular
4
+ monolith: each business capability lives in its own feature package, while
5
+ `cmd/api/wiring.go` is the process composition root. This file is the working
6
+ contract for AI coding agents and humans; keep generated structure and the
7
+ runtime safety rules below intact.
6
8
 
7
- ## Hard rule: use the CLI to add a new domain module or a new endpoint
9
+ ## Start every task here
8
10
 
9
- If you are about to create a **new** domain (a new `internal/app/<name>/`
10
- package with its own model/handler/service/repository), or add a **new**
11
- endpoint to an existing one stop and run the CLI instead:
11
+ 1. Read the relevant code, `docs/architect/`, tests, and this file before
12
+ editing. Search for an existing module or port before creating one.
13
+ 2. Check `git status --short --branch`. Work on a dedicated, non-protected
14
+ branch and preserve unrelated changes. Never edit a protected branch just
15
+ to make a quick fix.
16
+ 3. Classify the request: generated surface, business behavior, schema, auth /
17
+ security, infrastructure, or documentation. Ask the owner before changing
18
+ schema/migrations, authentication or security behavior, architecture,
19
+ dependencies, or deployment configuration when the project workflow
20
+ requires approval.
21
+ 4. Make the smallest complete change, add a focused test, and report exact
22
+ commands and output. Do not claim a check passed if it was skipped or
23
+ failed.
24
+
25
+ ## Architecture contract
26
+
27
+ This scaffold combines modular boundaries with Hexagonal Architecture,
28
+ domain-oriented design, and an optional CQRS style. They are compatible:
29
+ apply them inside each feature instead of creating a second architecture or
30
+ an empty layer for every noun.
31
+
32
+ - **Inbound adapter:** `handler.go` parses HTTP, validates/binds transport
33
+ input, calls an application service, and maps the result to HTTP.
34
+ - **Application boundary:** `service.go` (or `commands.go`/`queries.go` when
35
+ the module uses `--cqrs`; complex auth may also use
36
+ `internal/app/<feature>/application/`) orchestrates use cases. It owns
37
+ workflow decisions and depends on narrow consumer-owned ports.
38
+ - **Domain:** models, value rules, and invariants stay independent of Gin,
39
+ GORM, Redis, HTTP status codes, and environment variables.
40
+ - **Outbound adapter:** `repository.go`, token stores, mail/queue clients,
41
+ and other platform integrations translate a port into persistence or I/O.
42
+ - **Composition:** the feature-local `composition.go` constructs its
43
+ repository, service, and handler. `cmd/api/wiring.go` selects shared
44
+ infrastructure, supplies explicitly declared cross-feature ports/security
45
+ dependencies, and registers the feature route; it does not contain
46
+ business logic or feature-internal constructors.
47
+
48
+ The project defaults new modules to `{{defaultApplicationStyle}}` application
49
+ handling and a `{{defaultModuleSurface}}` surface. The `generate module` wizard
50
+ starts there through the named Lean/CRUD/CQRS profiles; `Advanced` exposes the
51
+ two axes for unusual combinations. `--profile` is the clear per-module
52
+ override, while `--cqrs`/`--full` remain supported for existing scripts, and
53
+ `go-scaffold config` changes defaults for future modules only.
54
+
55
+ Use CQRS when a feature has genuinely different command and query models,
56
+ consistency needs, or scaling/read-shape pressure: commands mutate state and
57
+ queries read state. For simple CRUD, one application service and one port are
58
+ clearer. Do not introduce a broker, a second database, or a command/query
59
+ folder only to satisfy a label.
60
+
61
+ Keep feature boundaries real. Do not import another feature's private model,
62
+ repository, or handler. Expose a small public application port, call a
63
+ documented feature API, or use an explicit event when cross-feature behavior
64
+ is required. Keep dependency direction flowing inward:
65
+
66
+ ```text
67
+ handler -> application/use case -> domain + ports -> adapters
68
+ ^ |
69
+ +--- composition -+
70
+ ```
71
+
72
+ The generated CRUD shape is an intentionally incremental starter. It gives
73
+ each module a testable service/handler seam and a feature-local composition
74
+ root; it does not decide the final aggregate, fields, invariants, or split
75
+ between commands and queries for the product.
76
+
77
+ ## Hard rule: use the CLI for generated surface
78
+
79
+ If you are creating a **new domain** (`internal/app/<name>/` with its own
80
+ model/service/repository/handler), or adding a **new endpoint** to an existing
81
+ domain, stop and run the CLI:
12
82
 
13
83
  ```bash
14
84
  go-scaffold generate module <name> --defaults
15
85
  go-scaffold generate method <module> <name> --type <get|post|put|patch|delete>
16
86
  ```
17
87
 
18
- **Pass every value as a flag.** Run interactively, these commands ask for
19
- anything you left out which has no answer in a non-interactive shell, so
20
- they exit 1 having written nothing. `--defaults` on `generate module` means
21
- "minimal, no auth, ask nothing"; add `--full`/`--auth`/`--permission` to it
22
- when you want those. Same rule elsewhere: `--get-mode` is required with
23
- `--type get`, and `undo module` needs `-y`. Every `add` command confirms
24
- before writing — `-y` skips that, but `add auth`/`add worker` also ask which
25
- backing store to use, so those two want `--defaults` (or `--store`/`--queue`
26
- plus `-y`).
27
-
28
- This applies **even when the request doesn't say "module"/"method" or name
29
- the CLI at all**. Recognize indirect asks as generation work, for example:
30
-
31
- - "add a products feature" → a new domain (`generate module products --defaults`)
32
- - "let admins approve orders" → a new method on an existing module
33
- (`generate method orders approve --type patch`)
34
- - "we need an endpoint that lists overdue invoices" a new `get` method
35
- (`generate method invoices findOverdue --type get --get-mode all`)
36
- - "add a way to look up a user by email"a new `get` lookup
37
- (`generate method users findByEmail --type get --get-mode one --field email`)
38
-
39
- Run `go-scaffold generate module --help` / `generate method --help` before
40
- hand-rolling anything that looks like scaffolding.
41
-
42
- ## What the CLI does NOT do — these are still done by hand, as normal
43
-
44
- - Business logic inside a generated method (the CLI leaves a TODO-marked
45
- stub filling in real behavior is your job)
46
- - Adding fields to a generated model/DTO beyond `id`/`created_at`/`updated_at`
47
- - Foreign keys / relations between domains add the column, the migration
48
- `REFERENCES ... ON DELETE ...` clause, and the `dberr.IsForeignKey` mapping
49
- yourself; see `docs/architect/patterns.md` for the 3 rules domains with a
50
- relation must follow
51
- - Bug fixes, refactors, or edits to existing logic
52
- - Anything in a project that does **not** have `go-scaffold.config.json` at
53
- its root — that means the project wasn't scaffolded by this CLI, and this
54
- rule does not apply
88
+ Pass every value as a flag in CI. In an interactive shell, omitted values use
89
+ the project defaults as the initial wizard choices. In a non-interactive
90
+ shell, omitted values prompt and the command exits without writing.
91
+ `--defaults` uses the recorded project defaults and no auth; use `--profile`
92
+ for a named per-module preset, or add `--full`, `--cqrs`, `--auth`, and
93
+ `--permission <code>` deliberately.
94
+ `--get-mode`
95
+ is required for `--type get`, and `undo module` needs `-y`. For `add auth`,
96
+ choose the backing store and browser topology with `--defaults` or explicit
97
+ flags (`--store`, `--browser-topology`) as well as the confirmation flag.
98
+ For `add worker`, choose the backing queue with `--defaults` or an explicit
99
+ flag.
100
+
101
+ Recognize indirect requests as generation work:
102
+
103
+ - “add a products feature” → `generate module products --defaults`
104
+ - “let admins approve orders” `generate method orders approve --type patch`
105
+ - “list overdue invoices” → `generate method invoices findOverdue --type get --get-mode all`
106
+ - look up a user by email`generate method users findByEmail --type get --get-mode one --field email`
107
+
108
+ Run `go-scaffold generate module --help` or `generate method --help` when the
109
+ desired shape is unclear. Do not hand-edit generated route markers to add a
110
+ new endpoint; the CLI keeps handler, service interface, test stub, and
111
+ OpenAPI/migration patching in sync.
112
+
113
+ ## What the CLI does not decide
114
+
115
+ These remain deliberate engineering work:
116
+
117
+ - business behavior, invariants, authorization policy, and real DTO/model
118
+ fields;
119
+ - validation beyond the transport shape and all domain-specific rules;
120
+ - foreign keys and relations between domains, including their reviewed SQL
121
+ and `dberr.IsForeignKey` mapping;
122
+ - the business-specific command/query contracts and the public port used for
123
+ cross-feature collaboration;
124
+ - bug fixes, refactors, and changes to an existing method;
125
+ - any work in a repository that lacks `go-scaffold.config.json`.
126
+
127
+ Generated methods are explicit stubs. `generate method` GET-all reuses
128
+ `FindAll` until real filtering is added; POST and DELETE return an internal
129
+ error until implemented; PUT/PATCH intentionally return `501 Not Implemented`
130
+ and must not read or write a record before their behavior is designed. Treat
131
+ the generated OpenAPI TODO contract as incomplete, not as a promise that the
132
+ endpoint is production-ready.
133
+
134
+ ## Runtime and schema safety
135
+
136
+ - `APP_ENV` is the single environment gate and accepts only `development` or
137
+ `production`. Do not add a parallel flag such as `AUTO_MIGRATE`.
138
+ - Development may use the convenience `db.AutoMigrate(...)` path for the
139
+ models registered by the generator. Production must never bootstrap or
140
+ mutate schema at API startup: apply versioned SQL with
141
+ `make migrate-up`, then let the startup migration-version check fail fast if
142
+ the database is behind or dirty.
143
+ - Any schema change is a reviewed pair of versioned files in `migrations/`.
144
+ Use `go-scaffold generate migration <name>` for changes outside a new
145
+ module, write both directions, and test them against a disposable database.
146
+ - Keep `*sql.DB`, Redis, queue, mail, and other external resources owned by
147
+ their composition root. Close them on every exit path and preserve the
148
+ signal-driven graceful shutdown; do not call `os.Exit` from a goroutine or
149
+ hide a startup error.
150
+ - Keep liveness and readiness semantics intact. Readiness may check external
151
+ dependencies; liveness must not turn a transient dependency failure into a
152
+ process restart.
153
+
154
+ ## Auth and error boundaries
155
+
156
+ Treat auth and security changes as high-risk and get the required owner
157
+ approval before changing their contract.
158
+
159
+ - Never log passwords, raw access/refresh/reset/verification tokens, JWT
160
+ secrets, OAuth client secrets, or authorization headers.
161
+ - Refresh rotation is an atomic consume: Postgres uses one SQL unit and Redis
162
+ uses one Lua operation, including active-session cleanup and the reuse
163
+ tombstone. Its inactivity expiry may move on rotation, but its persisted
164
+ absolute expiry never moves. Never replace it with a read-then-delete
165
+ sequence.
166
+ - Password reset and email verification consume a one-time token in the same
167
+ retry-safe transaction as the user/identity update. A failed post-commit
168
+ session revocation must not make a successful reset impossible to retry.
169
+ - The generated auth facade is a pragmatic compatibility boundary: its service
170
+ contracts currently use the feature's `model` values and shared `apperror`,
171
+ while handlers own Gin and provider adapters own SDK configuration. Do not
172
+ broaden that coupling by importing Gin, provider SDKs, or HTTP handlers into
173
+ use cases. New complex flows should prefer transport-neutral DTOs and narrow
174
+ ports; a deeper separation of the existing auth facade is an explicit
175
+ architecture refactor and needs focused tests.
176
+ - Browser provider exchange consumes a one-time server-side transaction bound
177
+ to provider, state, S256 PKCE challenge, and OIDC nonce. Google ID tokens
178
+ must be signature, issuer, audience/azp, time-claim, nonce, and subject
179
+ validated before identity resolution. Token responses use
180
+ `Cache-Control: no-store` and `Pragma: no-cache`; SameSite=None also needs an
181
+ exact allowed Origin guard in addition to CORS.
182
+
183
+ MFA has an operator capability switch and a separate per-user enrollment:
184
+
185
+ - `AUTH_MFA_ENABLED=false` is the default. When enabled, the composition root
186
+ must validate `MFA_ENCRYPTION_KEY` as a base64-encoded 32-byte key before
187
+ serving traffic. Keep the issuer, challenge TTL, TOTP window, and recovery
188
+ code count explicit and within their safety limits.
189
+ - Authenticated users manage enrollment through `GET /users/me/mfa`,
190
+ `POST /users/me/mfa/setup`, `POST /users/me/mfa/confirm`, and
191
+ `POST /users/me/mfa/disable` (the last requires the current TOTP code).
192
+ Setup material is pending-only; recovery codes are returned once.
193
+ - Password login and provider exchange may return a pre-session
194
+ `{ "mfa_required": true, "challenge": "..." }`. Do not issue an access
195
+ token or refresh cookie until `POST /auth/mfa/verify` succeeds. Challenges
196
+ are hashed, short-lived, and one-use; failed verification consumes the
197
+ challenge. Recovery codes are hashed, atomic one-use login factors and are
198
+ not accepted to disable MFA.
199
+ - MFA data is durable in Postgres for both refresh-token store choices. The
200
+ `create_mfa` migration must be applied before production and covered by
201
+ real-store/concurrency tests.
202
+
203
+ Keep auth construction explicit: `NewService(deps Dependencies, cfg
204
+ AuthConfig)`. `Dependencies` owns narrow ports such as `MFA`, `Providers`,
205
+ `RefreshTokens`, `RecoveryTokens`, `Mailer`, `Roles`, and `Clock`; `AuthConfig`
206
+ owns JWT, OAuth, recovery, and `MFASettings` policy. Do not pass the generated
207
+ shared config package into the application service or replace these contracts
208
+ with a positional constructor.
209
+ - Return a stable, generic client error for unexpected failures. Wrap causes
210
+ with `%w` or pass them to `apperror.NewInternal(cause)` so middleware can
211
+ log the server-side cause with `request_id`; production responses must not
212
+ expose that cause or internal field details.
213
+
214
+ ## Verification workflow
215
+
216
+ After changing Go templates or generated Go code, run the checks that match
217
+ the change (from the generated project root):
218
+
219
+ ```bash
220
+ gofmt -w <changed-go-files>
221
+ go test ./...
222
+ go test -race ./... # required for concurrency/auth/token changes
223
+ go vet ./...
224
+ golangci-lint run
225
+ ```
226
+
227
+ For schema/repository/auth adapter work, run the real-store tests with a
228
+ migrated disposable PostgreSQL/Redis instance and `REQUIRE_TEST_DB=true` or
229
+ `REQUIRE_TEST_REDIS=true`; a skipped integration test is not evidence of
230
+ correctness. MFA state still requires PostgreSQL even when refresh tokens use
231
+ Redis. When routes or OpenAPI templates change, inspect the updated
232
+ `docs/openapi.yaml` and run the configured OpenAPI linter.
233
+
234
+ Only in the `go-scaffold` source repository, run the generator project's
235
+ `pnpm run verify` for CLI/template changes, then generate fresh sample projects
236
+ to inspect the output. A generated Go project does not contain the generator's
237
+ `package.json` or pnpm scripts; from that project run the Go checks above and
238
+ use the installed/local CLI for generation checks.
239
+
240
+ At each checkpoint record: branch and base, files changed, generated output
241
+ shape, tests and their exact result, and any limitation. Before handoff,
242
+ review `git diff --check`, `git diff`, and `git status --short`; leave unrelated
243
+ work untouched.
55
244
 
56
245
  ## Command quick reference
57
246
 
58
- - `go-scaffold generate module <name> --defaults` — safe minimal model + errors +
59
- repository + service/handler plumbing, wired into `cmd/api/wiring.go` and
60
- appended to `migrations/`. Add endpoints one at a time with `generate
61
- method`, or pass `--full` to opt into a CRUD skeleton with TODO DTO fields
62
- - `go-scaffold generate method <module> <name> --type <get|post|put|patch|delete> [--get-mode all|one] [--field <name>]` —
63
- patches an *existing* module's handler + service (and repository, for a
64
- `get --get-mode one --field` lookup) in place; never overwrites a method
65
- with the same name — pick a different one if it collides. With OpenAPI
66
- enabled it also writes a valid TODO path document and wires the index;
67
- replace placeholder schemas while implementing the method
68
- - `go-scaffold undo module <name> -y` — takes back a `generate module` that
69
- shouldn't have happened (typo'd name, domain decided against): deletes the
70
- package, un-wires main.go/OpenAPI, and deletes the module's migration files.
71
- It refuses if those migrations are committed to git or already applied to
72
- your database, since then they may exist somewhere this can't reach. Not for
73
- retiring a domain that has shipped that's `generate migration drop_<table>`
74
- plus a reviewed data removal; the table is never dropped either way
75
- - Every route in this project is grouped under{{#if apiPrefix}} `/{{apiPrefix}}`{{else}} no prefix{{/if}}
76
- (set once at `create` time via `--api-prefix`) — there is no per-domain
77
- versioning; a breaking API change gets a new domain package or a new DTO
78
- field, not a duplicated model
79
- - Full architecture rationale for this project's configuration: see
80
- `docs/architect/`
247
+ - `go-scaffold generate module <name> --defaults` — minimal model, errors,
248
+ repository, service/handler seams, feature-local `composition.go`, root
249
+ route registration, and versioned module migrations. Add endpoints with
250
+ `generate method`, use `--profile crud` for a CRUD skeleton, or
251
+ `--profile cqrs` for separate command/query application handlers.
252
+ - `go-scaffold generate method <module> <name> --type <get|post|put|patch|delete> [--get-mode all|one] [--field <name>]` — patches an existing
253
+ module's handler/service and, for a GET-one lookup, repository plus test
254
+ seam. It never overwrites a same-named method.
255
+ - `go-scaffold generate migration <name>` reserves a timestamped SQL
256
+ migration pair for a reviewed schema change.
257
+ - `go-scaffold config` — interactively change future module defaults;
258
+ `config show` prints the resolved manifest and `config validate` checks it
259
+ without writing.
260
+ - `go-scaffold undo module <name> -y` removes an unshipped generated module,
261
+ its migration, and its wiring. It refuses once those migrations are
262
+ committed or applied; use a reviewed drop migration for a shipped domain.
263
+ - All routes share the project-wide prefix chosen at `create` time:
264
+ `{{#if apiPrefix}}/{{apiPrefix}}{{else}}(no prefix){{/if}}`. There is no
265
+ per-domain versioning.
266
+
267
+ Read `docs/architect/architecture.md`, `docs/architect/patterns.md`, and
268
+ `docs/architect/techstack.md` for the rationale behind these constraints.
@@ -68,7 +68,8 @@ tools:
68
68
  tidy:
69
69
  go mod tidy
70
70
 
71
- # creates the database itself (not the schema — AUTO_MIGRATE or migrate-up does that).
71
+ # creates the database itself (not the schema — the app's development path or
72
+ # migrate-up does that).
72
73
  # connects to the always-present "postgres" maintenance DB to run CREATE DATABASE,
73
74
  # since the target DB may not exist yet. Safe to re-run — skips if it already exists.
74
75
  # \gexec only works read from stdin, not through -c, hence the pipe.
@@ -2,14 +2,23 @@
2
2
 
3
3
  Gin + GORM backend, scaffolded by [go-scaffold](https://github.com/nakedev/go-scaffold). Organized by feature (domain), Postgres-backed, schema managed with [golang-migrate](https://github.com/golang-migrate/migrate).
4
4
 
5
- This project starts as a bare skeleton. Add a safe minimal domain with:
5
+ This project starts as a bare skeleton. Its defaults for future modules resolve
6
+ to `{{defaultModuleSurface}}` surface + `{{defaultApplicationStyle}}` application
7
+ style. The `generate module` wizard presents this as a named Lean, CRUD, CQRS,
8
+ or Advanced profile so you can choose by intent instead of memorising two
9
+ implementation axes. Add a domain with:
6
10
 
7
11
  ```bash
8
12
  go-scaffold generate module orders
9
13
  ```
10
14
 
11
- Add endpoints one at a time with `generate method`, or pass `--full` to opt
12
- into a CRUD skeleton whose DTO fields and business rules remain explicit TODOs.
15
+ The `generate module` wizard starts from those defaults. Add endpoints one at a
16
+ time with `generate method`, pass `--profile crud` for a CRUD skeleton, or use
17
+ `--profile cqrs` when commands and queries deserve separate application
18
+ handlers. DTO fields and business rules remain explicit TODOs. Run
19
+ `go-scaffold config` later to change defaults for future modules; existing
20
+ modules are unchanged. The older `--full` and `--cqrs` flags remain supported
21
+ for scripts.
13
22
 
14
23
  See `docs/architect/` for the conventions every generated module follows, and `AGENTS.md`/`CLAUDE.md` if you're working with an AI coding agent in this repo.
15
24
 
@@ -18,7 +27,8 @@ See `docs/architect/` for the conventions every generated module follows, and `A
18
27
  ```
19
28
  cmd/
20
29
  └── api/
21
- └── main.go # config, slog, graceful shutdown, wires domains — the only entry point for now
30
+ ├── main.go # process entry point, config, slog, graceful shutdown
31
+ └── wiring.go # composition root: infrastructure + domain registration
22
32
  internal/
23
33
  ├── platform/ # talks to real external systems (DB, later: cache, queue, mail, ...)
24
34
  │ └── database/ # opens the connection + pool (GORM)
@@ -42,7 +52,7 @@ make docker-up # local Postgres (postgres:5432)
42
52
  {{/if}}
43
53
  make db-create # create the {{dbName}} database itself (once — safe to re-run)
44
54
  go mod tidy
45
- make run # AUTO_MIGRATE=true creates the schema automatically in dev
55
+ make run # APP_ENV=development enables the convenience schema bootstrap
46
56
  ```
47
57
 
48
58
  `make run`/`make test`/`make migrate-up`/`make migrate-down` load `.env` if
@@ -103,10 +113,11 @@ migrate -path migrations -database "$DB_DSN" down 1
103
113
  migrate create -ext sql -dir migrations -seq=false add_something # same as `go-scaffold generate migration`, if you'd rather not use the CLI
104
114
  ```
105
115
 
106
- **dev:** leave `AUTO_MIGRATE=true` (default) so GORM's AutoMigrate creates the schema quickly.
107
- **prod:** set `AUTO_MIGRATE=false` and run `migrate up` as a separate deploy step — versioned, has rollback (`down`), doesn't lock the table the way AutoMigrate does once there's real data.
116
+ **dev:** `APP_ENV=development` enables the convenience table bootstrap.
117
+ **prod:** `APP_ENV=production` never bootstraps or mutates the schema; run
118
+ `migrate up` as a separate deploy step — versioned and rollback-capable.
108
119
 
109
- With `AUTO_MIGRATE=false`, the app checks the DB's applied migration version
120
+ In production, the app checks the DB's applied migration version
110
121
  against the migration files baked into the binary (embedded at build time)
111
122
  before it starts serving traffic — a stale or half-applied schema fails fast
112
123
  at boot with a clear message, instead of failing later on whatever query
@@ -128,12 +139,25 @@ make migrate-verify # up -> down-to-zero -> up, catches a broken down.sql befo
128
139
  | `PORT` | `8080` | |
129
140
  | `DB_DSN` | `postgres://postgres:postgres@localhost:5432/{{dbName}}?sslmode=disable` | used by both GORM and the `migrate` CLI |
130
141
  | `LOG_LEVEL` | `info` | debug/info/warn/error |
131
- | `AUTO_MIGRATE` | `true` | disable in prod and use `migrate up` (see Migrations) |
132
142
  | `DB_MAX_OPEN_CONNS` | `10` | |
133
143
  | `DB_MAX_IDLE_CONNS` | `10` | |
134
144
  | `DB_CONN_MAX_LIFETIME_MIN` | `5` | minutes |
135
145
  | `CORS_ALLOWED_ORIGINS` | `http://localhost:3000` | comma-separated frontend origins allowed to call this API with credentials (cookies) |
136
146
 
147
+ After `go-scaffold add auth`, `.env.example` also contains
148
+ `GOOGLE_OAUTH_REDIRECT_URI` (the exact browser callback URI registered with
149
+ Google), `JWT_REFRESH_MAX_TTL_MIN`, `OAUTH_STATE_TTL_MIN`, and
150
+ `AUTH_BROWSER_TOPOLOGY`. Refresh rotation uses the configured inactivity TTL
151
+ but never moves a token past its absolute lifetime. The backend stores a
152
+ one-time OAuth transaction binding provider, state, S256 challenge, and OIDC
153
+ nonce; the frontend still owns the callback route and sends the code to the
154
+ exchange endpoint. The provider redirect URI is separate from
155
+ `CORS_ALLOWED_ORIGINS`; the latter remains an exact-origin credentialed API
156
+ policy. Token responses use `Cache-Control: no-store` and `Pragma: no-cache`.
157
+ For `cross-site`, configure HTTPS origins, `COOKIE_SAMESITE=none`,
158
+ `COOKIE_SECURE=true`, and an exact allowed Origin; the API applies an Origin
159
+ guard independently of CORS.
160
+
137
161
  ## Tests
138
162
 
139
163
  Handler and service tests are fast unit tests: handlers depend on a narrow
@@ -166,15 +190,15 @@ TEST_DB_DSN=postgres://postgres:postgres@localhost:5432/{{dbName}}_test?sslmode=
166
190
 
167
191
  ## API spec
168
192
 
169
- `docs/openapi.yaml` is hand-written and served, along with every file it `$ref`s (`common/`, `health/`, per-domain folders), under `/docs` while the server runspoint a renderer (Scalar, Swagger UI, Redoc) or a client generator (Hey API) at `http://localhost:8080/docs/openapi.yaml`, or browse the tree at `http://localhost:8080/docs/`. Update the spec by hand whenever an endpoint/DTO changes.
193
+ `docs/openapi.yaml` is hand-written and split across sibling files (`common/`, `health/`, per-domain folders) via relative `$ref`. **The server never serves it** there is no `/docs` route, in any environment, so the API surface and `docs/architect/`'s internal notes stay off the wire. Update the spec by hand whenever an endpoint/DTO changes.
170
194
 
171
- Some tools don't resolve external `$ref`s when importing a local file — they read it as-is and see zero routes (Bruno's "Import Collection" does this; some Postman flows too). For those, bundle the spec into one fully-resolved file first:
195
+ To open it in a renderer (Scalar, Swagger UI, Redoc) or a client generator (Hey API), bundle it into one fully-resolved file and point the tool at that file. Bundling is also what importers that don't resolve external `$ref`s need — they read the index as-is and see zero routes (Bruno's "Import Collection" does this; some Postman flows too):
172
196
 
173
197
  ```bash
174
198
  make openapi-bundle # writes docs/openapi.bundled.yaml (gitignored — regenerate anytime)
175
199
  ```
176
200
 
177
- Import `docs/openapi.bundled.yaml` instead. Tools that resolve `$ref` over HTTP or when reading a URL (Scalar, Swagger UI, Redoc) don't need this point them at the live `/docs/openapi.yaml` URL above.
201
+ Then open `docs/openapi.bundled.yaml` — it needs no network and no running server.
178
202
  {{/if}}
179
203
 
180
204
  ## Adding a domain
@@ -183,10 +207,12 @@ Import `docs/openapi.bundled.yaml` instead. Tools that resolve `$ref` over HTTP
183
207
  go-scaffold generate module orders
184
208
  ```
185
209
 
186
- Scaffolds the safe minimal `internal/app/order/` module, wires its empty route
187
- group/model into `cmd/api/wiring.go`, and appends a migration file. Add endpoints
188
- with `generate method`; use `--full` only when a CRUD skeleton is intentional.
189
- See `docs/architect/patterns.md` for the module shape and foreign-key rules.
210
+ Scaffolds an `internal/app/order/` module using the project defaults
211
+ (`{{defaultModuleSurface}}` + `{{defaultApplicationStyle}}`), wires its route/
212
+ model into `cmd/api/wiring.go`, and appends a migration file. Add endpoints
213
+ with `generate method`; use `--profile crud` for a CRUD skeleton or
214
+ `--profile cqrs` for separate command/query handlers. See
215
+ `docs/architect/patterns.md` for the module shape and foreign-key rules.
190
216
 
191
217
  ## Deploying
192
218
 
@@ -213,10 +239,12 @@ Before the first deploy of a release:
213
239
 
214
240
  | | |
215
241
  |---|---|
216
- | `AUTO_MIGRATE` | leave unset it defaults off when `APP_ENV=production`, and the app checks the applied migration version instead |
242
+ | `APP_ENV` | `production` | production disables schema bootstrap and requires the applied migration version |
217
243
  | migrations | `migrate -path migrations -database "$DB_DSN" up` as its own step, before the new binaries roll |
218
244
  | `TRUSTED_PROXIES` | the CIDRs of your ingress, or the auth rate limiter keys on a header anyone can send |
219
245
  | `COOKIE_SECURE` | `true` · `COOKIE_SAMESITE=none` as well if your frontend is on a different site |
246
+ | `JWT_REFRESH_MAX_TTL_MIN` | `43200` | absolute refresh-token lifetime; rotation cannot extend beyond it |
247
+ | `OAUTH_STATE_TTL_MIN` | `10` | lifetime of the one-time server-side OAuth state transaction |
220
248
  | `/metrics` | reachable in-cluster for Prometheus, blocked at the ingress |
221
249
  | `JWT_SECRET`, `SMTP_HOST` | the app refuses to start in production without real values |
222
250
 
@@ -46,17 +46,20 @@ func run() error {
46
46
  if err != nil {
47
47
  return fmt.Errorf("db handle: %w", err)
48
48
  }
49
+ defer func() {
50
+ if err := sqlDB.Close(); err != nil {
51
+ logger.Error("close db", "error", err)
52
+ }
53
+ }()
49
54
  // go-scaffold:platform-init
50
55
 
51
- // AutoMigrate creates tables but never the schema they live in — each
52
- // domain gets its own (see model.go's TableName), so it has to exist
53
- // before AutoMigrate runs. The versioned SQL migrations create the same
54
- // schemas with CREATE SCHEMA IF NOT EXISTS, so this only matters for dev.
55
- // go-scaffold:schemas
56
+ if !cfg.IsProd() {
57
+ // Development boot creates tables but never the schema they live in — each
58
+ // domain gets its own (see model.go's TableName), so it has to exist
59
+ // before the bootstrap runs. Production runs the versioned SQL migrations
60
+ // instead, so this entire branch is skipped there.
61
+ // go-scaffold:schemas
56
62
 
57
- if cfg.AutoMigrate {
58
- // ponytail: AutoMigrate is for dev only (add-only, locks the table once data grows)
59
- // prod: set AUTO_MIGRATE=false and run golang-migrate as versioned SQL instead
60
63
  if err := db.AutoMigrate(
61
64
  // go-scaffold:models
62
65
  ); err != nil {
@@ -89,23 +92,13 @@ func run() error {
89
92
  })
90
93
  // go-scaffold:extra-routes
91
94
  {{#if openapiDocs}}
92
- // hand-written spec at docs/openapi.yaml, split across sibling files (common/, health/,
93
- // <domain>/) via relative $ref serve the whole tree under one prefix so a client that
94
- // resolves $ref over HTTP (Scalar, Swagger UI, Redoc, Hey API pointed at a URL) can reach
95
- // them too; StaticFile on just the index file would 404 on every $ref it follows.
96
- //
97
- // gin.Dir(..., true) keeps directory listings on, which r.Static suppresses:
98
- // without them opening /docs/ in a browser answers 404 with a blank page,
99
- // which reads as "this is broken" rather than "ask for a file". The listing
100
- // costs nothing here — every file in the tree is already downloadable — and
101
- // it makes openapi.yaml discoverable without knowing its name up front.
102
- //
103
- // Not in production: this serves your whole API surface — every path,
104
- // parameter and schema — to anyone who asks. Publish the spec deliberately
105
- // (`make openapi-bundle`) rather than by leaving this on.
106
- if !cfg.IsProd() {
107
- r.StaticFS("/docs", gin.Dir("./docs", true))
108
- }
95
+ // ponytail: docs/ is deliberately NOT served over HTTP, in any environment.
96
+ // The whole treeevery path, parameter and schema, plus docs/architect/'s
97
+ // internal design notes used to be reachable at /docs outside production.
98
+ // Read the spec from the working copy instead: `make openapi-bundle` writes
99
+ // one fully-resolved docs/openapi.bundled.yaml for a renderer or client
100
+ // generator to open as a local file. Publish it deliberately if you want
101
+ // it public.
109
102
  {{/if}}
110
103
 
111
104
  api := r.Group("/{{apiPrefix}}")
@@ -20,7 +20,8 @@ import (
20
20
 
21
21
  // Open connects to Postgres and sets the connection pool — it talks to a
22
22
  // real external system, so it lives in platform/, not shared/.
23
- // Schema is managed via golang-migrate (migrations/) in prod, not AutoMigrate — see README.
23
+ // Production schema is managed via golang-migrate (migrations/); development
24
+ // may use the convenience path selected by APP_ENV in the composition root.
24
25
  func Open(cfg config.Config) (*gorm.DB, error) {
25
26
  // GORM's default logger reports ErrRecordNotFound at ERROR level, so an
26
27
  // ordinary miss — every FindByID behind a 404, cmd/seed's "does this admin
@@ -58,8 +59,7 @@ var migrationVersionRe = regexp.MustCompile(`^(\d+)_.*\.up\.sql$`)
58
59
  // golang-migrate CLI's own schema_migrations table) doesn't match the newest
59
60
  // migration file baked into this binary — instead of booting against a stale
60
61
  // or half-applied schema and failing later on whatever query hits the
61
- // missing column first. Only meaningful when AutoMigrate is off (prod); call
62
- // this from that branch only.
62
+ // missing column first. Call this from the production branch only.
63
63
  func CheckMigrationVersion(db *gorm.DB) error {
64
64
  entries, err := migrations.FS.ReadDir(".")
65
65
  if err != nil {
@@ -10,10 +10,15 @@ type AppError struct {
10
10
  Message string `json:"message"`
11
11
  Details any `json:"details,omitempty"`
12
12
  RequestID string `json:"request_id,omitempty"` // filled in by the error middleware
13
+ cause error
13
14
  }
14
15
 
15
16
  func (e *AppError) Error() string { return e.Message }
16
17
 
18
+ // Unwrap keeps the technical cause available to structured logging and
19
+ // errors.Is/As without putting it in the JSON response.
20
+ func (e *AppError) Unwrap() error { return e.cause }
21
+
17
22
  func New(status int, code, msg string) *AppError {
18
23
  return &AppError{HTTPStatus: status, Code: code, Message: msg}
19
24
  }
@@ -30,7 +35,15 @@ func NewConflict(msg string) *AppError {
30
35
  return &AppError{HTTPStatus: http.StatusConflict, Code: "CONFLICT", Message: msg}
31
36
  }
32
37
 
33
- func NewInternal() *AppError {
38
+ func NewInternal(cause ...error) *AppError {
34
39
  // ponytail: never leak internal details to the client, log separately in middleware
35
- return &AppError{HTTPStatus: http.StatusInternalServerError, Code: "INTERNAL", Message: "internal server error"}
40
+ err := &AppError{HTTPStatus: http.StatusInternalServerError, Code: "INTERNAL", Message: "internal server error"}
41
+ if len(cause) > 0 {
42
+ err.cause = cause[0]
43
+ }
44
+ return err
45
+ }
46
+
47
+ func NewNotImplemented() *AppError {
48
+ return &AppError{HTTPStatus: http.StatusNotImplemented, Code: "NOT_IMPLEMENTED", Message: "not implemented"}
36
49
  }