@nakedev/go-scaffold 0.4.0 → 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 (118) 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 +42 -1
  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/platform-patcher.js +29 -7
  28. package/dist/utils/rbac-patcher.js +97 -75
  29. package/package.json +7 -2
  30. package/templates/add/auth/cmd/seed/main.go.hbs +13 -3
  31. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  32. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  33. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  34. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  35. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  36. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  37. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  38. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  39. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  40. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  41. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  42. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  43. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  44. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +113 -0
  45. package/templates/add/auth/internal/app/user/browser_policy.go.hbs +98 -0
  46. package/templates/add/auth/internal/app/user/composition.go.hbs +165 -0
  47. package/templates/add/auth/internal/app/user/contracts.go.hbs +88 -0
  48. package/templates/add/auth/internal/app/user/dto.go.hbs +57 -0
  49. package/templates/add/auth/internal/app/user/errors.go.hbs +25 -0
  50. package/templates/add/auth/internal/app/user/external_login.go.hbs +208 -0
  51. package/templates/add/auth/internal/app/user/handler.go.hbs +60 -203
  52. package/templates/add/auth/internal/app/user/handler_local.go.hbs +75 -0
  53. package/templates/add/auth/internal/app/user/handler_mfa.go.hbs +83 -0
  54. package/templates/add/auth/internal/app/user/handler_oauth.go.hbs +70 -0
  55. package/templates/add/auth/internal/app/user/handler_recovery.go.hbs +49 -0
  56. package/templates/add/auth/internal/app/user/handler_test.go.hbs +290 -0
  57. package/templates/add/auth/internal/app/user/handler_user.go.hbs +41 -0
  58. package/templates/add/auth/internal/app/user/jwt.go.hbs +6 -59
  59. package/templates/add/auth/internal/app/user/local_auth.go.hbs +98 -0
  60. package/templates/add/auth/internal/app/user/mfa_service.go.hbs +450 -0
  61. package/templates/add/auth/internal/app/user/mfa_service_test.go.hbs +199 -0
  62. package/templates/add/auth/internal/app/user/mfa_store.go.hbs +127 -0
  63. package/templates/add/auth/internal/app/user/mfa_store_test.go.hbs +174 -0
  64. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +8 -2
  65. package/templates/add/auth/internal/app/user/model/identity.go.hbs +4 -3
  66. package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +17 -0
  67. package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +20 -0
  68. package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +17 -0
  69. package/templates/add/auth/internal/app/user/model/user.go.hbs +3 -2
  70. package/templates/add/auth/internal/app/user/provider_test.go.hbs +286 -0
  71. package/templates/add/auth/internal/app/user/recovery_service.go.hbs +114 -0
  72. package/templates/add/auth/internal/app/user/repository.go.hbs +2 -0
  73. package/templates/add/auth/internal/app/user/service.go.hbs +82 -478
  74. package/templates/add/auth/internal/app/user/service_test.go.hbs +601 -45
  75. package/templates/add/auth/internal/app/user/session_cookie.go.hbs +33 -0
  76. package/templates/add/auth/internal/app/user/sessions.go.hbs +99 -0
  77. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +42 -14
  78. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +105 -40
  79. package/templates/add/auth/internal/app/user/tokenstore_pg_test.go.hbs +96 -0
  80. package/templates/add/auth/internal/app/user/tokenstore_recovery.go.hbs +58 -0
  81. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +144 -70
  82. package/templates/add/auth/internal/app/user/tokenstore_redis_test.go.hbs +185 -0
  83. package/templates/add/auth/internal/app/user/user_query.go.hbs +65 -0
  84. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  85. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  86. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +9 -4
  87. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  88. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  89. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  90. package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -2
  91. package/templates/add/rbac/internal/app/role/composition.go.hbs +35 -0
  92. package/templates/add/rbac/internal/app/role/service.go.hbs +12 -12
  93. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +340 -121
  94. package/templates/create/base/.env.example.hbs +0 -1
  95. package/templates/create/base/AGENTS.md.hbs +255 -67
  96. package/templates/create/base/Makefile.hbs +2 -1
  97. package/templates/create/base/README.md.hbs +42 -14
  98. package/templates/create/base/cmd/api/wiring.go.hbs +11 -8
  99. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  100. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  101. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  102. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  103. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  104. package/templates/create/features/docs/architecture.md.hbs +35 -13
  105. package/templates/create/features/docs/patterns.md.hbs +40 -21
  106. package/templates/create/features/docs/techstack.md.hbs +2 -2
  107. package/templates/generate/module/commands.go.hbs +95 -0
  108. package/templates/generate/module/composition.go.hbs +23 -0
  109. package/templates/generate/module/cqrs_test.go.hbs +7 -0
  110. package/templates/generate/module/handler.go.hbs +50 -5
  111. package/templates/generate/module/minimal/commands.go.hbs +34 -0
  112. package/templates/generate/module/minimal/handler.go.hbs +34 -0
  113. package/templates/generate/module/minimal/queries.go.hbs +45 -0
  114. package/templates/generate/module/minimal/service.go.hbs +27 -1
  115. package/templates/generate/module/queries.go.hbs +62 -0
  116. package/templates/generate/module/service.go.hbs +61 -5
  117. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  118. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
package/README.md CHANGED
@@ -4,7 +4,8 @@ A CLI that scaffolds a Gin + GORM + PostgreSQL Go backend, then keeps
4
4
  generating consistent domain modules into that project as it grows — the Go
5
5
  counterpart to nest-scaffold.
6
6
 
7
- You don't hand-wire a new domain into `cmd/api/wiring.go`, write the
7
+ You don't hand-wire a new domain's repository/service/handler composition into
8
+ `cmd/api/wiring.go`, write the
8
9
  handler/service/repository boilerplate, or decide error-handling conventions
9
10
  each time — the CLI does that, and every module it generates follows the
10
11
  same shape as the last one.
@@ -33,6 +34,84 @@ node bin/go-scaffold.js create my-api --defaults
33
34
  depending on your machine's npm/pnpm global-bin config — running
34
35
  `node bin/go-scaffold.js ...` directly sidesteps that.
35
36
 
37
+ ## Release: npm is locked to Git
38
+
39
+ Git is the release source of truth. The package version, annotated tag, and
40
+ tagged commit must agree before npm can publish:
41
+
42
+ ```text
43
+ package.json 0.4.3
44
+
45
+ └── annotated tag v0.4.3 ──> the exact release commit
46
+
47
+ └── npm @nakedev/go-scaffold@0.4.3
48
+ ```
49
+
50
+ `npm publish` runs `release:check` first, so publishing from an untagged,
51
+ lightweight-tagged, dirty, or mismatched checkout fails before anything is
52
+ sent to npm. The GitHub Actions workflow at
53
+ `.github/workflows/release.yml` repeats that check, runs the full verification
54
+ gate, publishes from the tag, and creates the GitHub Release.
55
+
56
+ ### One-time npm setup
57
+
58
+ The preferred setup is npm Trusted Publishing with GitHub Actions (OIDC):
59
+
60
+ 1. In npm package settings for `@nakedev/go-scaffold`, add a GitHub Actions
61
+ trusted publisher for `NakePranob/go-scaffold`.
62
+ 2. Set the workflow filename to `release.yml` and environment to
63
+ `npm-release`.
64
+ 3. Allow `npm publish`. The workflow already requests the required OIDC
65
+ permission and uses Node 24.
66
+
67
+ If Trusted Publishing is not used, add an npm granular token as the GitHub
68
+ repository/environment secret `NPM_TOKEN`. Never commit a token or put it in
69
+ `.npmrc`.
70
+
71
+ ### Branch policy
72
+
73
+ `develop` is the integration branch. A PR into `main` must come from
74
+ `develop` or `release/*`, and must bump `package.json` to a semver greater than
75
+ the version on `main`. PRs into `develop` do not need a version bump.
76
+
77
+ The `main-merge-policy` workflow checks this automatically. In GitHub branch
78
+ rules, protect `main`, require a pull request, require the
79
+ `main-merge-policy`, `verify`, and `packaged-artifact` checks, and disable
80
+ force-pushes. Protect the `v*` tag pattern from updates and deletion as well.
81
+
82
+ ### Release flow
83
+
84
+ ```bash
85
+ git switch develop
86
+ git pull --ff-only origin develop
87
+ git switch -c release/v0.4.3
88
+ npm version 0.4.3 --no-git-tag-version
89
+ pnpm run verify
90
+ git add package.json
91
+ git commit -m "chore: release v0.4.3"
92
+ git push -u origin release/v0.4.3
93
+ # open a PR from release/v0.4.3 into main
94
+
95
+ # after the PR is merged, tag the merge commit on main
96
+ git switch main
97
+ git pull --ff-only origin main
98
+ git tag -a v0.4.3 -m "v0.4.3"
99
+ git push origin v0.4.3
100
+ ```
101
+
102
+ Pushing the tag starts the release workflow. To retry a failed publish for a
103
+ tag created after this workflow landed, use GitHub Actions' **Run workflow**
104
+ with that tag; do not create a second tag for the same package version. A tag
105
+ created before this guard existed should not be moved after it has been
106
+ pushed — use the next patch version instead.
107
+
108
+ ## Requirements
109
+
110
+ - Node.js `>=22.13` to run the CLI
111
+ - Go `>=1.25` for the generated project
112
+ - A running PostgreSQL instance; Docker is optional, but either `psql` or a
113
+ Docker container is needed by `make db-create`
114
+
36
115
  ## Quick start
37
116
 
38
117
  ```bash
@@ -51,12 +130,60 @@ go-scaffold generate module orders
51
130
  go-scaffold generate method orders approve --type patch
52
131
  ```
53
132
 
133
+ ## The 30-second flow
134
+
135
+ The CLI has two layers of interaction: the command you choose, then only the
136
+ questions that command still needs. Flags are answers, not requests to ask the
137
+ same question again.
138
+
139
+ ```text
140
+ go-scaffold create my-api
141
+ 1. Docker + PostgreSQL?
142
+ 2. OpenAPI files?
143
+ 3. Metrics + tracing?
144
+ 4. API route prefix?
145
+ 5. Default module profile: Lean / CRUD / CQRS / Advanced?
146
+ 6. Summary confirmation
147
+
148
+ cd my-api
149
+ go-scaffold generate module users
150
+ 1. Module profile: Lean / CRUD / CQRS / Advanced?
151
+ 2. Require an access token? # only when auth is installed
152
+ 3. Permission code? # only when auth + RBAC are installed
153
+ -> writes internal/app/user/, its migration, wiring, and config metadata
154
+ ```
155
+
156
+ `Advanced` is the only path that asks the two lower-level architecture
157
+ questions separately. `--profile` and `--defaults` are the scripted forms; the
158
+ latter uses the project defaults recorded in `go-scaffold.config.json`.
159
+
54
160
  ## Commands
55
161
 
56
162
  Every `add` command shows what it's about to do and asks before writing;
57
163
  `-y/--yes` skips that (and `--defaults` implies it) for CI and scripts.
58
164
  Running `go-scaffold` with no arguments picks the command from a menu.
59
165
 
166
+ ### Wizard coverage
167
+
168
+ The interactive path is deliberately available from both the bare command and
169
+ the direct command form:
170
+
171
+ | Command | Wizard coverage |
172
+ |---|---|
173
+ | `create [name]` | asks for the project name and settings that were not passed as flags |
174
+ | `generate` / `generate module [name]` | chooses a target, module name, and module profile; `Advanced` asks the two underlying architecture questions |
175
+ | `generate method [module] [name]` | asks for the existing module, method name, HTTP verb, GET mode, and lookup field when needed |
176
+ | `generate migration [name]` | asks for the migration name when omitted |
177
+ | `config` | edits future module defaults; existing modules are unchanged |
178
+ | `config show` / `config validate` | intentionally no wizard: read-only print/validation commands |
179
+ | `add` / `add worker` / `add auth` | chooses the feature, backend/topology, and confirmation where applicable |
180
+ | `add rbac` / `add observability` | no parameter choice is needed; the direct command confirms, while bare `add` selects the target |
181
+ | `undo` / `undo module [name]` | asks for the generated module and confirmation when omitted |
182
+
183
+ Run any command with `--help` for the non-interactive equivalent. If a value is
184
+ omitted in a non-TTY shell, the CLI exits before writing and tells you which
185
+ flag or `--defaults` is required.
186
+
60
187
 
61
188
  ### `create <name>` — scaffold a new project
62
189
 
@@ -74,15 +201,21 @@ with `generate module`.
74
201
 
75
202
  | Option | Effect |
76
203
  |---|---|
77
- | `--defaults` | Skip the wizard, use defaults (Docker on, OpenAPI docs on, no route prefix) |
78
- | `--no-docker` | Skip `docker-compose.yml` |
79
- | `--no-openapi-docs` | Skip `docs/openapi.yaml` |
80
- | `--observability` | Prometheus `/metrics` + OpenTelemetry tracing (off by default — `add observability` does the same later) |
81
- | `--api-prefix <prefix>` | URL prefix every route is grouped under opt in with e.g. `v1` or `api/v1`; omit it for none |
82
-
83
- Without `--defaults`, an interactive wizard asks the same four questions
204
+ | `--defaults` | Skip settings prompts; use Docker/OpenAPI on, no prefix, and Lean (`minimal + service`) defaults for future modules |
205
+ | `--no-docker` | Do not create `docker-compose.yml` or include a local Postgres service |
206
+ | `--no-openapi-docs` | Do not create `docs/openapi.yaml` or per-module OpenAPI files |
207
+ | `--observability` | Include Prometheus `/metrics` + OpenTelemetry tracing; off unless passed |
208
+ | `--api-prefix <prefix>` | Group every API route under a prefix such as `v1` or `api/v1`; omit for no prefix |
209
+ | `--module-profile <lean\|crud\|cqrs>` | Default profile for future modules; replaces the two architecture questions |
210
+ | `--module-surface <minimal\|crud>` | Legacy axis flag for future modules; use `--module-profile` for a clearer preset |
211
+ | `--application-style <service\|cqrs>` | Legacy axis flag for future modules; use `--module-profile` for a clearer preset |
212
+
213
+ Without `--defaults`, an interactive wizard asks the project questions —
84
214
  skipping any a flag already answered, so `create my-api --no-docker` never asks
85
- about Docker and never scaffolds it.
215
+ about Docker and never scaffolds it. It also asks for the default module
216
+ profile so future `generate module` commands start with the project's
217
+ conventions. Choose `Advanced` when you intentionally want the less common
218
+ CRUD + CQRS combination.
86
219
  The prefix is a single project-wide choice made once at `create` time —
87
220
  there's no per-domain versioning (a domain that needs a real breaking change
88
221
  gets a new domain package or a new DTO field, not a duplicated model pointed
@@ -91,18 +224,72 @@ below).
91
224
 
92
225
  **Config file** — every `create` writes `go-scaffold.config.json` to the
93
226
  project root; `generate` reads it back (or auto-detects from `go.mod` /
94
- directory layout if missing).
227
+ directory layout if missing). It records project defaults and the resolved
228
+ surface/application style of each generated module:
229
+
230
+ ```json
231
+ {
232
+ "schemaVersion": 1,
233
+ "architecture": {
234
+ "style": "modular-monolith",
235
+ "defaultModuleSurface": "minimal",
236
+ "defaultApplicationStyle": "service"
237
+ },
238
+ "modules": {
239
+ "order": { "surface": "crud", "applicationStyle": "cqrs" }
240
+ }
241
+ }
242
+ ```
243
+
244
+ Run these from the generated project root. Use the wizard again later without
245
+ recreating the project:
246
+
247
+ ```bash
248
+ go-scaffold config # interactive project-default wizard
249
+ go-scaffold config show # print the resolved config
250
+ go-scaffold config validate # validate without changing anything
251
+ ```
252
+
253
+ ### Module profiles
254
+
255
+ The wizard asks for one useful profile instead of forcing everyone to reason
256
+ about two implementation axes up front:
257
+
258
+ | Profile | Resolves to | Use it when |
259
+ |---|---|---|
260
+ | `lean` | minimal surface + one service | the domain should start small and grow endpoint by endpoint |
261
+ | `crud` | CRUD surface + one service | the domain genuinely needs the standard list/get/create/update/delete starter |
262
+ | `cqrs` | minimal surface + command/query handlers | reads and writes have different business models, invariants, or scaling pressure |
263
+ | `Advanced` (wizard only) | choose both axes separately | you deliberately want a custom mix, including CRUD + CQRS |
264
+
265
+ The scaffold is DDD-shaped rather than a complete tactical DDD implementation:
266
+ it gives each domain a package boundary, repository port, application boundary,
267
+ feature-local composition, and shared error conventions. It does not invent
268
+ aggregates, value objects, domain events, or business invariants for you; those
269
+ belong to the domain team.
270
+
271
+ `minimal` means the generator does not invent five endpoints before the domain
272
+ has real requirements. `CQRS` does not mean a second database, broker, or event
273
+ bus here. It only separates command and query application handlers inside the
274
+ same modular monolith.
95
275
 
96
276
  ### `generate module <name>` (alias `m`) — add a domain module
97
277
 
98
278
  ```bash
99
- go-scaffold generate module orders # asks for the shape (and auth, if installed)
100
- go-scaffold generate module orders --full # opt-in CRUD skeleton, no prompt
101
- go-scaffold generate module orders --defaults # safe minimal module, no prompt (CI/scripting)
279
+ go-scaffold generate module orders # asks for profile (and auth, if installed)
280
+ go-scaffold generate module orders --profile lean # explicit Lean profile, no architecture prompt
281
+ go-scaffold generate module orders --profile crud # explicit CRUD profile, no architecture prompt
282
+ go-scaffold generate module orders --profile cqrs # explicit CQRS profile, no architecture prompt
283
+ go-scaffold generate module orders --full --cqrs # legacy flags: CRUD + separate command/query handlers
284
+ go-scaffold generate module orders --defaults # use project defaults, no prompt (CI/scripting)
102
285
  ```
103
286
 
104
- Anything you don't pass as a flag is asked for; `--defaults` takes the
105
- documented defaults (minimal, no auth) and asks nothing.
287
+ Anything you don't pass as a flag is asked for; the prompt starts with the
288
+ project defaults. `--defaults` uses those defaults without asking anything
289
+ (fresh and legacy projects default to Lean, with no auth). `--profile` is the
290
+ non-interactive equivalent of choosing a named profile for this module. The
291
+ older `--full` and `--cqrs` flags remain supported for existing scripts; do not
292
+ combine them with `--profile`.
106
293
 
107
294
  `--full` scaffolds:
108
295
 
@@ -113,12 +300,30 @@ internal/app/order/
113
300
  ├── errors.go # ORDER_NOT_FOUND / ORDER_CONFLICT / ORDER_HAS_REFERENCES / ORDER_STALE
114
301
  ├── repository.go # GORM data access
115
302
  ├── service.go # business logic + repository interface (mockable)
303
+ ├── composition.go # feature-local repository → service → handler wiring
116
304
  ├── handler.go # Gin routes, registered under the project's API prefix
117
305
  ├── service_test.go # unit test, function-backed repository stub
118
306
  ├── handler_test.go # HTTP unit test, service stub, no DB
119
307
  └── repository_test.go # Postgres integration test against migrated schema
120
308
  ```
121
309
 
310
+ With `--cqrs`, the module also adds separate command/query application files:
311
+
312
+ ```text
313
+ internal/app/order/
314
+ ├── commands.go # command port + state-changing application handlers
315
+ ├── queries.go # query port + read-only application handlers
316
+ ├── service.go # compatibility facade; new wiring uses both handlers
317
+ ├── composition.go # constructs command/query handlers separately
318
+ └── cqrs_test.go # command/query boundary tests
319
+ ```
320
+
321
+ `--cqrs` works with both minimal and `--full` modules. It keeps one modular
322
+ monolith and one database by default; CQRS here means separate application
323
+ paths, not mandatory separate databases, brokers, or event sourcing. The
324
+ default remains the simpler layered module because an empty command/query
325
+ split adds ceremony without a business reason.
326
+
122
327
  The default minimal mode scaffolds the same `model`/`errors`/`repository` (so `generate
123
328
  method` always has a full data-access surface to call), but `dto`/`service`/
124
329
  `handler` start empty — no default CRUD, no routes, just the plumbing
@@ -128,13 +333,15 @@ surface, or you'd rather add endpoints one at a time.
128
333
 
129
334
  Both modes also:
130
335
 
131
- - Register the module in `cmd/api/wiring.go` (via marker comments see
132
- `// go-scaffold:*` in that file) — full wires an actual route, minimal
133
- wires an empty route group
336
+ - Register the module through its feature-local composition and the root
337
+ registration markers in `cmd/api/wiring.go` — full wires an actual route,
338
+ minimal wires an empty route group
134
339
  - Create the module's own Postgres schema (`<module>_svc`, e.g. `order_svc`)
135
- and add the model to the `AutoMigrate(...)` call
340
+ and add the model to the development schema bootstrap
136
341
  - Append `migrations/<timestamp>_create_<plural>.{up,down}.sql`, which creates that
137
- same schema for `AUTO_MIGRATE=false`/production
342
+ same schema for production
343
+ - Record the resolved `minimal|crud` and `service|cqrs` choices in
344
+ `go-scaffold.config.json`; changing project defaults does not rewrite existing modules
138
345
 
139
346
  What it does **not** do: invent your fields or wire foreign keys between
140
347
  domains — see `docs/architect/patterns.md` in the generated project for the
@@ -148,9 +355,12 @@ go-scaffold generate method orders findByStatus --type get --get-mode one --fiel
148
355
  go-scaffold g me orders findOverdue --type get --get-mode all
149
356
  ```
150
357
 
151
- Patches an *existing* module's `handler.go`/`service.go` in place via the
152
- same marker-comment approach as `main.go` — never a whole new module. Never
153
- overwrites a method with the same name; picks a different one or errors.
358
+ Patches an *existing* module's `handler.go`/`service.go` in place at the
359
+ `// go-scaffold:*` markers — never a whole new module. Never overwrites a method
360
+ with the same name; pick a different one or the command errors.
361
+ For a module generated with `--cqrs`, it also patches `commands.go` for
362
+ state-changing endpoints and `queries.go` for read endpoints, while keeping
363
+ the compatibility facade in sync.
154
364
 
155
365
  | Option | Effect |
156
366
  |---|---|
@@ -185,8 +395,8 @@ the generated code doesn't compile, but this project was fine a moment ago.
185
395
  The most likely cause is drift: this project's internal/shared layer has been edited
186
396
  since it was scaffolded, so the templates this CLI emits no longer match it.
187
397
 
188
- scaffolded with: go-scaffold 0.1.2
189
- this CLI: go-scaffold 0.3.0
398
+ scaffolded with: go-scaffold <project-version>
399
+ this CLI: go-scaffold <cli-version>
190
400
  ```
191
401
 
192
402
  That happens because `generate`'s templates are written against the `shared/`
@@ -224,7 +434,6 @@ async email delivery, and `cmd/worker`.
224
434
  | Extra service to run | none | Redis |
225
435
  | Needed by `add auth` | no | no — `add auth --store` decides that separately |
226
436
  | Enqueue joins your DB transaction | yes | **no** — needs an outbox |
227
- | Throughput | thousands/sec | tens of thousands/sec |
228
437
  | Inspect pending jobs | plain SQL | asynqmon |
229
438
 
230
439
  The default is Postgres because a job enqueued inside `tx.Do` is then only
@@ -251,26 +460,51 @@ tx.Do(ctx, db, func(ctx context.Context) error {
251
460
  ### `add auth` — add email/password authentication
252
461
 
253
462
  ```bash
254
- go-scaffold add auth # asks where tokens should live, then confirms
463
+ go-scaffold add auth # asks token store + browser topology, then confirms
255
464
  go-scaffold add auth --store postgres # tokens in Postgres, no extra service
256
465
  go-scaffold add auth --store redis # tokens in Redis, exact across replicas
257
- go-scaffold add auth --defaults # Postgres, no prompt at all (CI/scripting)
466
+ go-scaffold add auth --defaults # Postgres + local same-site topology (CI/scripting)
467
+ go-scaffold add auth --browser-topology cross-site --yes
258
468
  ```
259
469
 
260
470
  Adds JWT access tokens, refresh-token rotation with reuse detection,
261
471
  registration/login/logout, password reset, email verification, failed-login
262
- lockout, and Google OAuth routes. Apply the generated migrations;
263
- `AUTO_MIGRATE=true` is convenient in development, while production should use
264
- `migrate up`.
265
-
266
- No prerequisites. On a project with no worker the verification and reset mail
267
- is sent inline, and `add worker` later moves it onto the queue for you — the
268
- two endpoints that send mail block on SMTP until you do.
269
-
270
- | `--store` | Tokens and rate-limit counters | Extra service |
472
+ lockout, and generic provider OAuth routes (Google is the first adapter). Apply
473
+ the generated migrations. Development may bootstrap tables for convenience;
474
+ production must run `migrate up` first.
475
+
476
+ The browser frontend owns its single provider callback route. It generates
477
+ `state` and an S256 PKCE verifier/challenge, starts
478
+ `GET /auth/{provider}/login`, handles both success and provider-cancel/error
479
+ responses in that route, then sends `code`, `state`, and `code_verifier` to
480
+ `POST /auth/{provider}/exchange`. The API uses the exact
481
+ `GOOGLE_OAUTH_REDIRECT_URI` registered with the provider, creates the local
482
+ session, sets the HttpOnly refresh cookie, and returns JSON. The backend also
483
+ consumes a one-time transaction binding provider, state, S256 challenge, and
484
+ OIDC nonce before completing the exchange. It never accepts a
485
+ request-supplied `redirect_uri`/`return_to`, redirects to a configured frontend
486
+ URL, or places tokens/code/state in a URI. Native/mobile flow is out of scope
487
+ for this scaffold phase.
488
+
489
+ `AUTH_BROWSER_TOPOLOGY` is only the cookie/CORS deployment policy, separate
490
+ from the provider redirect URI. For a genuinely cross-site frontend use
491
+ `--browser-topology cross-site`, deploy over HTTPS, set
492
+ `COOKIE_SAMESITE=none` and `COOKIE_SECURE=true`, and add the exact frontend
493
+ origin to `CORS_ALLOWED_ORIGINS` separately. SameSite=None requests also pass
494
+ an exact Origin guard because CORS alone is not CSRF protection. Token
495
+ responses use `Cache-Control: no-store` and `Pragma: no-cache`; configure
496
+ `JWT_REFRESH_MAX_TTL_MIN` so refresh rotation cannot extend beyond its absolute
497
+ lifetime.
498
+
499
+ No prerequisites. On a project with no worker, the registration-verification,
500
+ resend-verification, and password-reset mail flows are sent inline, and `add
501
+ worker` later moves them onto the queue for you. Until then, those auth flows
502
+ block on SMTP when a mail server is configured.
503
+
504
+ | `--store` | Refresh/recovery tokens and rate-limit counters | Extra service |
271
505
  |---|---|---|
272
506
  | `postgres` (default) | `user_svc.auth_tokens`, counters in-process | none |
273
- | `redis` | Redis | Redis |
507
+ | `redis` | refresh + rate-limit counters in Redis; recovery in `user_svc.auth_tokens` | Redis |
274
508
 
275
509
  The rate limiter follows the store rather than being chosen separately, because
276
510
  "I want this exact across replicas" is one decision. With `postgres` the per-IP
@@ -286,8 +520,8 @@ go-scaffold generate module secrets --auth --permission secret:manage
286
520
 
287
521
  Requires `add auth`. Adds role/permission administration, cached authorization
288
522
  middleware, and role assignment. Its migration seeds the default roles and
289
- permissions, so apply it with `migrate up`: AutoMigrate creates tables but does
290
- not run SQL seed statements.
523
+ permissions, so apply it with `migrate up`; table creation does not run SQL
524
+ seed statements.
291
525
 
292
526
  ### `add observability` — add metrics + tracing
293
527
 
@@ -313,8 +547,8 @@ go-scaffold undo m orders --yes # skip the confirm
313
547
  The inverse of `generate module`, for the case it's actually the inverse of:
314
548
  a module you didn't mean to generate — a typo'd name, a domain you decided
315
549
  against. It deletes `internal/app/<name>/`, the per-module docs folder, **and
316
- the module's migration files**, and reverses the import/AutoMigrate/route in
317
- `main.go` plus the paths/schemas in `docs/openapi.yaml`. Restores the
550
+ the module's migration files**, and reverses the import/bootstrap/route in
551
+ `cmd/api/wiring.go` plus the paths/schemas in `docs/openapi.yaml`. Restores the
318
552
  `_ = api` placeholder if it was the last module, so the project still builds.
319
553
 
320
554
  Deleting the migrations is the point. `migrations/embed.go` is a `//go:embed
@@ -331,9 +565,10 @@ working tree, so `undo` proves it first and refuses loudly otherwise:
331
565
  `migrate ... down` first, then try again.
332
566
 
333
567
  The table itself is never dropped either way — `undo` only reverses what the
334
- CLI wrote. Prefer it to hand-deleting the folder: it also un-wires `main.go`,
335
- `.golangci.yml` and the OpenAPI index, and it refuses when another domain
336
- still imports this one rather than leaving you an un-compilable project.
568
+ CLI wrote. Prefer it to hand-deleting the folder: it also un-wires
569
+ `cmd/api/wiring.go`, `.golangci.yml` and the OpenAPI index, and it refuses when
570
+ another domain still imports this one rather than leaving you an un-compilable
571
+ project.
337
572
 
338
573
  ## Why no per-domain versioning
339
574
 
@@ -343,8 +578,8 @@ twice with different behavior. It was cut: the migration (and usually the
343
578
  DB table) is shared between "versions" of the same domain, but each version
344
579
  got its own physically-copied `model.go` — nothing stopped the two structs
345
580
  from drifting apart. Verified against a real Postgres instance:
346
- `AutoMigrate` silently accepted a column typed `int` in one version's model
347
- and `float64` in the other for the *same* column, converging it to
581
+ development schema bootstrapping silently accepted a column typed `int` in
582
+ one version's model and `float64` in the other for the *same* column, converging it to
348
583
  `numeric` with no error — the two versions would then read/write the same
349
584
  data with different, silently incompatible interpretations.
350
585
 
@@ -357,7 +592,9 @@ out of sync with.
357
592
  ## Project structure produced by `create`
358
593
 
359
594
  ```text
360
- cmd/api/wiring.go
595
+ cmd/api/
596
+ ├── main.go # process entry point and exit handling
597
+ └── wiring.go # composition root: infrastructure + domain registration
361
598
  internal/
362
599
  ├── platform/database/
363
600
  ├── shared/{config,apperror,dberr,httpx,id,middleware,pagination,tx}/
@@ -383,8 +620,9 @@ go-scaffold.config.json
383
620
 
384
621
  ## Supported stack
385
622
 
386
- Pinned in the generated `go.mod` — this table mirrors
387
- `templates/create/base/go.mod.hbs`, which is the source of truth.
623
+ Pinned base dependencies in the generated `go.mod` — this table mirrors
624
+ `templates/create/base/go.mod.hbs`, which is the source of truth. `add auth`,
625
+ `add worker`, and `add observability` append their optional dependencies.
388
626
 
389
627
  | Package | Version |
390
628
  |---|---|
@@ -18,6 +18,7 @@ const migrations_1 = require("../utils/migrations");
18
18
  const openapi_patcher_1 = require("../utils/openapi-patcher");
19
19
  const gocheck_1 = require("../utils/gocheck");
20
20
  const gomod_patcher_1 = require("../utils/gomod-patcher");
21
+ const auth_wizard_1 = require("../prompts/auth-wizard");
21
22
  // URL (relative to the api prefix) -> docs file (relative to docs/) for every
22
23
  // route `add auth` registers — kept next to AUTH_FILES's route list so the
23
24
  // two are easy to eyeball together when a route changes.
@@ -29,20 +30,26 @@ const AUTH_OPENAPI_PATHS = [
29
30
  { urlPath: "/auth/forgot-password", file: "./auth/forgot-password.yaml" },
30
31
  { urlPath: "/auth/reset-password", file: "./auth/reset-password.yaml" },
31
32
  { urlPath: "/auth/verify-email", file: "./auth/verify-email.yaml" },
32
- { urlPath: "/auth/google/login", file: "./auth/google-login.yaml" },
33
- { urlPath: "/auth/google/callback", file: "./auth/google-callback.yaml" },
33
+ { urlPath: "/auth/{provider}/login", file: "./auth/provider-login.yaml" },
34
+ { urlPath: "/auth/{provider}/exchange", file: "./auth/provider-exchange.yaml" },
34
35
  { urlPath: "/users/me", file: "./auth/users-me.yaml" },
35
36
  { urlPath: "/users/me/resend-verification", file: "./auth/users-me-resend-verification.yaml" },
36
37
  { urlPath: "/users/me/logout-all", file: "./auth/users-me-logout-all.yaml" },
38
+ { urlPath: "/users/me/mfa", file: "./auth/users-me-mfa.yaml" },
39
+ { urlPath: "/users/me/mfa/setup", file: "./auth/users-me-mfa-setup.yaml" },
40
+ { urlPath: "/users/me/mfa/confirm", file: "./auth/users-me-mfa-confirm.yaml" },
41
+ { urlPath: "/users/me/mfa/disable", file: "./auth/users-me-mfa-disable.yaml" },
42
+ { urlPath: "/auth/mfa/verify", file: "./auth/mfa-verify.yaml" },
37
43
  ];
38
44
  // addAuth scaffolds email/password authentication: a users+identities model
39
- // pair, JWT access tokens, a Redis-backed refresh token store with
45
+ // pair, JWT access tokens, a selectable refresh token store with
40
46
  // rotation + reuse detection, and register/login/refresh/logout/me. No RBAC
41
47
  // (no roles/permissions) — that's a separate opt-in on top of this, since
42
48
  // most projects need "is this caller logged in" long before they need "can
43
49
  // this caller do X".
44
- async function addAuth(store = "postgres", projectDir = process.cwd()) {
50
+ async function addAuth(store = "postgres", projectDir = process.cwd(), browserTopology = auth_wizard_1.DEFAULT_BROWSER_TOPOLOGY) {
45
51
  const config = (0, config_1.readConfig)(projectDir);
52
+ const browser = (0, auth_wizard_1.validateBrowserTopology)(browserTopology);
46
53
  // No longer a prerequisite. Without a worker the verification and reset mail
47
54
  // goes out inline instead of through a queue — a real trade (those two
48
55
  // endpoints then block on SMTP), but not one worth forcing a second binary
@@ -63,7 +70,11 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
63
70
  (0, platform_patcher_1.patchConfigForSMTP)(path_1.default.join(projectDir, "internal", "shared", "config", "config.go"));
64
71
  patchEnvExampleForSMTP(path_1.default.join(projectDir, ".env.example"));
65
72
  }
66
- await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), { goModule: config.goModule });
73
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, (0, auth_manifest_1.authFiles)(store), {
74
+ goModule: config.goModule,
75
+ redis: store === "redis",
76
+ worker,
77
+ });
67
78
  const migrationsDir = path_1.default.join(projectDir, "migrations");
68
79
  fs_extra_1.default.ensureDirSync(migrationsDir);
69
80
  const usersVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
@@ -71,6 +82,11 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
71
82
  { template: "add/auth/migrations/create_users.up.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.up.sql`) },
72
83
  { template: "add/auth/migrations/create_users.down.sql.hbs", output: path_1.default.join("migrations", `${usersVersion}_create_users.down.sql`) },
73
84
  ], {});
85
+ const mfaVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
86
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
87
+ { template: "add/auth/migrations/create_mfa.up.sql.hbs", output: path_1.default.join("migrations", `${mfaVersion}_create_mfa.up.sql`) },
88
+ { template: "add/auth/migrations/create_mfa.down.sql.hbs", output: path_1.default.join("migrations", `${mfaVersion}_create_mfa.down.sql`) },
89
+ ], {});
74
90
  // identities references users(id) — must apply strictly after it. A
75
91
  // second newMigrationVersion() call, scanning the dir again now that the
76
92
  // users pair is already written, guarantees a later (or same-second,
@@ -87,13 +103,13 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
87
103
  { template: "add/auth/migrations/create_login_throttle.up.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.up.sql`) },
88
104
  { template: "add/auth/migrations/create_login_throttle.down.sql.hbs", output: path_1.default.join("migrations", `${throttleVersion}_create_login_throttle.down.sql`) },
89
105
  ], {});
90
- if (store === "postgres") {
91
- const authTokensVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
92
- await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
93
- { template: "add/auth/migrations/create_auth_tokens.up.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.up.sql`) },
94
- { template: "add/auth/migrations/create_auth_tokens.down.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.down.sql`) },
95
- ], {});
96
- }
106
+ // Recovery tokens are always stored in Postgres so consumption and the user
107
+ // update can share one transaction, even when refresh rotation uses Redis.
108
+ const authTokensVersion = (0, migrations_1.newMigrationVersion)(migrationsDir);
109
+ await (0, template_renderer_1.applyTemplateEntries)(projectDir, [
110
+ { template: "add/auth/migrations/create_auth_tokens.up.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.up.sql`) },
111
+ { template: "add/auth/migrations/create_auth_tokens.down.sql.hbs", output: path_1.default.join("migrations", `${authTokensVersion}_create_auth_tokens.down.sql`) },
112
+ ], {});
97
113
  // Only meaningful when there is a worker; readConfig fills this from the
98
114
  // adapter file on disk, so the only way it is still unknown is a project
99
115
  // that has internal/platform/queue with neither adapter in it. Guessing
@@ -120,7 +136,7 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
120
136
  // go-redis only when something in this project actually constructs a client
121
137
  ...(store === "redis" ? ["github.com/redis/go-redis/v9 v9.22.0"] : []),
122
138
  ]);
123
- patchEnvExample(path_1.default.join(projectDir, ".env.example"));
139
+ patchEnvExample(path_1.default.join(projectDir, ".env.example"), browser);
124
140
  patchMakefile(path_1.default.join(projectDir, "Makefile"));
125
141
  let docsMessage = "";
126
142
  const openapiPath = path_1.default.join(projectDir, "docs", "openapi.yaml");
@@ -146,13 +162,14 @@ async function addAuth(store = "postgres", projectDir = process.cwd()) {
146
162
  ? "verification + password-reset mail goes through the queue"
147
163
  : "verification + password-reset mail is sent inline (no worker) — run `add worker` later to move it onto the queue");
148
164
  console.log(store === "postgres"
149
- ? "tokens + rate-limit counters: Postgres (user_svc.auth_tokens) and in-process — no Redis"
150
- : "tokens + rate-limit counters: Redis");
165
+ ? "refresh + recovery tokens: Postgres (user_svc.auth_tokens), rate-limit counters in-process — no Redis"
166
+ : "refresh tokens + rate-limit counters: Redis; recovery tokens: Postgres (user_svc.auth_tokens)");
151
167
  console.log("registered POST /auth/{register,login,refresh,logout,forgot-password,reset-password,verify-email}, " +
152
- "GET /auth/google/{login,callback}, GET /users/me, and " +
153
- "POST /users/me/{resend-verification,logout-all} in cmd/api/wiring.go" +
168
+ "GET /auth/{provider}/login, POST /auth/{provider}/exchange, GET /users/me, and " +
169
+ "POST /users/me/{resend-verification,logout-all,mfa/setup,mfa/confirm,mfa/disable}, " +
170
+ "GET /users/me/mfa, and POST /auth/mfa/verify in cmd/api/wiring.go" +
154
171
  docsMessage);
155
- console.log(picocolors_1.default.dim("\nnext: go mod tidy, then apply the new migrations (AUTO_MIGRATE=true picks them up automatically in dev)\n" +
172
+ console.log(picocolors_1.default.dim("\nnext: go mod tidy, then apply the new migrations with `make migrate-up` before production\n" +
156
173
  "seed an admin: SEED_ADMIN_EMAIL=... SEED_ADMIN_PASSWORD=... make seed"));
157
174
  }
158
175
  function patchMakefile(makefilePath) {
@@ -183,7 +200,7 @@ function patchMakefile(makefilePath) {
183
200
  content = content.replace(/\nbuild:/, () => `${target}\nbuild:`);
184
201
  fs_extra_1.default.writeFileSync(makefilePath, content);
185
202
  }
186
- function patchEnvExample(envExamplePath) {
203
+ function patchEnvExample(envExamplePath, browserTopology) {
187
204
  if (!fs_extra_1.default.existsSync(envExamplePath))
188
205
  return;
189
206
  let content = fs_extra_1.default.readFileSync(envExamplePath, "utf8");
@@ -195,13 +212,15 @@ function patchEnvExample(envExamplePath) {
195
212
  "JWT_SECRET=dev-secret-change-me\n" +
196
213
  "JWT_ACCESS_TTL_MIN=15\n" +
197
214
  "JWT_REFRESH_TTL_MIN=43200\n" +
198
- "COOKIE_SECURE=false\n" +
215
+ "JWT_REFRESH_MAX_TTL_MIN=43200\n" +
216
+ "OAUTH_STATE_TTL_MIN=10\n" +
217
+ `COOKIE_SECURE=${browserTopology === "cross-site" ? "true" : "false"}\n` +
199
218
  "# strict | lax | none — the refresh cookie's SameSite. Keep strict while the\n" +
200
219
  "# frontend is the same site as this API (localhost:3000 -> localhost:8080 is,\n" +
201
220
  "# and so is app.example.com -> api.example.com). A frontend on a different\n" +
202
221
  "# site entirely needs none, together with COOKIE_SECURE=true, or the browser\n" +
203
222
  "# never sends the cookie to /auth/refresh and sessions die at every expiry.\n" +
204
- "COOKIE_SAMESITE=strict\n" +
223
+ `COOKIE_SAMESITE=${browserTopology === "cross-site" ? "none" : "strict"}\n` +
205
224
  "\nPASSWORD_RESET_TTL_MIN=30\n" +
206
225
  "PASSWORD_RESET_URL=http://localhost:3000/reset-password\n" +
207
226
  "\nEMAIL_VERIFY_TTL_MIN=1440\n" +
@@ -209,7 +228,19 @@ function patchEnvExample(envExamplePath) {
209
228
  "\n# leave the Google vars unset to disable Google login (register/login/refresh still work)\n" +
210
229
  "GOOGLE_CLIENT_ID=\n" +
211
230
  "GOOGLE_CLIENT_SECRET=\n" +
212
- "GOOGLE_REDIRECT_URL=\n";
231
+ "# exact browser callback URI registered with the provider (frontend-owned route)\n" +
232
+ "GOOGLE_OAUTH_REDIRECT_URI=\n" +
233
+ "\n# cookie/CORS deployment topology; the frontend owns its provider callback route\n" +
234
+ `AUTH_BROWSER_TOPOLOGY=${browserTopology}\n` +
235
+ "\n# MFA is globally off by default. When enabled, set a base64-encoded 32-byte\n" +
236
+ "# AES-256 key (for example: openssl rand -base64 32). Users still opt in\n" +
237
+ "# individually through /users/me/mfa/setup and /users/me/mfa/confirm.\n" +
238
+ "AUTH_MFA_ENABLED=false\n" +
239
+ "MFA_ISSUER=go-scaffold\n" +
240
+ "MFA_ENCRYPTION_KEY=\n" +
241
+ "MFA_CHALLENGE_TTL_MIN=5\n" +
242
+ "MFA_TOTP_WINDOW=1\n" +
243
+ "MFA_RECOVERY_CODE_COUNT=10\n";
213
244
  fs_extra_1.default.writeFileSync(envExamplePath, content);
214
245
  }
215
246
  // ensureRedis adds internal/platform/cache + its config/env/main.go wiring