@nakedev/go-scaffold 0.4.3 → 0.5.0

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 (164) hide show
  1. package/README.md +567 -513
  2. package/dist/commands/auth.js +12 -1
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/create.js +2 -1
  5. package/dist/commands/generate.js +5 -2
  6. package/dist/commands/method.js +66 -70
  7. package/dist/commands/observability.js +4 -53
  8. package/dist/commands/rbac.js +19 -8
  9. package/dist/commands/undo.js +6 -3
  10. package/dist/commands/worker.js +14 -4
  11. package/dist/index.js +13 -1
  12. package/dist/templates/auth-manifest.js +46 -45
  13. package/dist/templates/create-manifest.js +4 -0
  14. package/dist/templates/module-manifest.js +82 -60
  15. package/dist/templates/rbac-manifest.js +15 -11
  16. package/dist/templates/worker-manifest.js +4 -1
  17. package/dist/types.js +2 -0
  18. package/dist/utils/auth-patcher.js +22 -22
  19. package/dist/utils/config.js +24 -5
  20. package/dist/utils/docs-patcher.js +68 -0
  21. package/dist/utils/hexagonal-method-patcher.js +334 -0
  22. package/dist/utils/main-patcher.js +3 -3
  23. package/dist/utils/module-location.js +17 -11
  24. package/dist/utils/platform-patcher.js +27 -0
  25. package/dist/utils/rbac-patcher.js +73 -216
  26. package/package.json +1 -1
  27. package/templates/add/auth/cmd/seed/main.go.hbs +2 -0
  28. package/templates/add/auth/internal/app/user/{browser_policy.go.hbs → adapters/inbound/http/browser_policy.go.hbs} +3 -3
  29. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  30. package/templates/add/auth/internal/app/user/{handler.go.hbs → adapters/inbound/http/handler.go.hbs} +103 -8
  31. package/templates/add/auth/internal/app/user/{handler_local.go.hbs → adapters/inbound/http/handler_local.go.hbs} +8 -7
  32. package/templates/add/auth/internal/app/user/{handler_mfa.go.hbs → adapters/inbound/http/handler_mfa.go.hbs} +6 -6
  33. package/templates/add/auth/internal/app/user/{handler_oauth.go.hbs → adapters/inbound/http/handler_oauth.go.hbs} +17 -17
  34. package/templates/add/auth/internal/app/user/{handler_recovery.go.hbs → adapters/inbound/http/handler_recovery.go.hbs} +4 -4
  35. package/templates/add/auth/internal/app/user/{handler_test.go.hbs → adapters/inbound/http/handler_test.go.hbs} +52 -31
  36. package/templates/add/auth/internal/app/user/{handler_user.go.hbs → adapters/inbound/http/handler_user.go.hbs} +4 -4
  37. package/templates/add/auth/internal/app/user/{session_cookie.go.hbs → adapters/inbound/http/session_cookie.go.hbs} +5 -3
  38. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  39. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  40. package/templates/add/auth/internal/app/user/{mfa_store.go.hbs → adapters/outbound/postgres/mfa_store.go.hbs} +22 -20
  41. package/templates/add/auth/internal/app/user/{mfa_store_test.go.hbs → adapters/outbound/postgres/mfa_store_test.go.hbs} +7 -7
  42. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  43. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  44. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  45. package/templates/add/auth/internal/app/user/{tokenstore_pg.go.hbs → adapters/outbound/postgres/tokenstore_pg.go.hbs} +36 -32
  46. package/templates/add/auth/internal/app/user/{tokenstore_pg_test.go.hbs → adapters/outbound/postgres/tokenstore_pg_test.go.hbs} +10 -3
  47. package/templates/add/auth/internal/app/user/{tokenstore_recovery.go.hbs → adapters/outbound/postgres/tokenstore_recovery.go.hbs} +29 -3
  48. package/templates/add/auth/internal/app/user/{tokenstore_redis.go.hbs → adapters/outbound/redis/tokenstore.go.hbs} +43 -36
  49. package/templates/add/auth/internal/app/user/{tokenstore_redis_test.go.hbs → adapters/outbound/redis/tokenstore_test.go.hbs} +14 -3
  50. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  51. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  52. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  53. package/templates/add/auth/internal/app/user/{external_login.go.hbs → application/external_login.go.hbs} +30 -40
  54. package/templates/add/auth/internal/app/user/{jwt.go.hbs → application/jwt.go.hbs} +6 -3
  55. package/templates/add/auth/internal/app/user/{local_auth.go.hbs → application/local_auth.go.hbs} +17 -19
  56. package/templates/add/auth/internal/app/user/{mfa_service.go.hbs → application/mfa_service.go.hbs} +27 -28
  57. package/templates/add/auth/internal/app/user/{mfa_service_test.go.hbs → application/mfa_service_test.go.hbs} +12 -11
  58. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +3 -3
  59. package/templates/add/auth/internal/app/user/{provider_test.go.hbs → application/provider_test.go.hbs} +55 -56
  60. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +16 -47
  61. package/templates/add/auth/internal/app/user/{recovery_service.go.hbs → application/recovery_service.go.hbs} +13 -15
  62. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  63. package/templates/add/auth/internal/app/user/{service_test.go.hbs → application/service_test.go.hbs} +94 -75
  64. package/templates/add/auth/internal/app/user/{sessions.go.hbs → application/sessions.go.hbs} +14 -14
  65. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  66. package/templates/add/auth/internal/app/user/{user_query.go.hbs → application/user_query.go.hbs} +16 -16
  67. package/templates/add/auth/internal/app/user/composition.go.hbs +89 -86
  68. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  69. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  70. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  71. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  72. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +1 -1
  73. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  74. package/templates/add/auth/migrations/create_users.up.sql.hbs +2 -1
  75. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  76. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  77. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  78. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  79. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  80. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  81. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  82. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  83. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  84. package/templates/add/rbac/internal/app/role/composition.go.hbs +23 -10
  85. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  86. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  87. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  88. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  89. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  90. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  91. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +54 -36
  92. package/templates/create/base/.golangci.yml.hbs +2 -2
  93. package/templates/create/base/AGENTS.md.hbs +55 -31
  94. package/templates/create/base/README.md.hbs +77 -22
  95. package/templates/create/base/cmd/api/wiring.go.hbs +3 -2
  96. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  97. package/templates/create/features/docs/architecture.md.hbs +64 -26
  98. package/templates/create/features/docs/patterns.md.hbs +117 -90
  99. package/templates/create/features/docs/techstack.md.hbs +17 -2
  100. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  101. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  102. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  103. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  104. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  105. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  106. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  107. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  108. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  109. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  110. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  111. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  112. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  113. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  114. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  115. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  116. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  117. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  118. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  119. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  120. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  121. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  122. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  123. package/templates/generate/module/migration.up.sql.hbs +1 -1
  124. package/dist/utils/method-patcher.js +0 -537
  125. package/templates/add/auth/internal/app/user/contracts.go.hbs +0 -88
  126. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -134
  127. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -68
  128. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -45
  129. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -32
  130. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  131. package/templates/add/auth/internal/app/user/model/mfa_challenge.go.hbs +0 -17
  132. package/templates/add/auth/internal/app/user/model/mfa_enrollment.go.hbs +0 -20
  133. package/templates/add/auth/internal/app/user/model/mfa_recovery_code.go.hbs +0 -17
  134. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -31
  135. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -139
  136. package/templates/add/auth/internal/app/user/service.go.hbs +0 -135
  137. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -58
  138. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  139. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  140. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  141. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  142. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  143. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  144. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  145. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  146. package/templates/generate/module/commands.go.hbs +0 -95
  147. package/templates/generate/module/composition.go.hbs +0 -23
  148. package/templates/generate/module/cqrs_test.go.hbs +0 -7
  149. package/templates/generate/module/dto.go.hbs +0 -36
  150. package/templates/generate/module/errors.go.hbs +0 -33
  151. package/templates/generate/module/handler.go.hbs +0 -179
  152. package/templates/generate/module/handler_test.go.hbs +0 -174
  153. package/templates/generate/module/minimal/commands.go.hbs +0 -34
  154. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  155. package/templates/generate/module/minimal/handler.go.hbs +0 -82
  156. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  157. package/templates/generate/module/minimal/queries.go.hbs +0 -45
  158. package/templates/generate/module/minimal/service.go.hbs +0 -71
  159. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  160. package/templates/generate/module/model/model.go.hbs +0 -36
  161. package/templates/generate/module/queries.go.hbs +0 -62
  162. package/templates/generate/module/repository.go.hbs +0 -103
  163. package/templates/generate/module/service.go.hbs +0 -164
  164. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -0,0 +1,7 @@
1
+ // Package composition contains process-level wiring and cross-feature adapters.
2
+ //
3
+ // Feature-local construction belongs in internal/app/<feature>/composition.go.
4
+ // Keep this package focused on translating caller-owned ports to another
5
+ // feature's public application capabilities. Do not put business rules,
6
+ // persistence adapters, or HTTP handlers here.
7
+ package composition
@@ -2,15 +2,17 @@
2
2
 
3
3
  ## Composition root
4
4
 
5
- `cmd/api/main.go` decides the exit code and nothing else. Everything it used to
6
- do lives in `cmd/api/wiring.go`, in a `run() error` — the one function that sees
7
- every module and hands them what they need.
5
+ `cmd/api/main.go` decides the exit code and nothing else. `cmd/api/wiring.go`
6
+ owns process bootstrap and route registration; `internal/composition/` owns
7
+ reusable process-level adapters that connect feature ports.
8
8
 
9
9
  Modules never import one another. Each generated domain owns its local
10
- repository → service handler composition in `composition.go`; `wiring.go`
11
- supplies the DB and any cross-feature security dependencies. A domain that
12
- needs another's behaviour declares a narrow interface for exactly what it
13
- needs and receives the concrete service from `run()`; `.golangci.yml` has a
10
+ outbound adapter → service or command/query application handlers inbound
11
+ adapter composition in `composition.go`; `internal/composition/` owns
12
+ cross-feature adapters and `wiring.go` supplies infrastructure, invokes those
13
+ adapters, and registers routes. A domain that needs another's behaviour
14
+ declares a narrow interface for exactly what it needs and receives its
15
+ concrete implementation from the process composition; `.golangci.yml` has a
14
16
  depguard rule per domain that fails the build if anyone shortcuts that.
15
17
 
16
18
  Two things the split buys. `run()` returns an error instead of calling
@@ -21,32 +23,49 @@ you open to understand the binary: `wiring.go` grows with the system, which is
21
23
  what a composition root is for, while `main.go` stays put.
22
24
 
23
25
 
24
- > Status: generated by `@nakedev/go-scaffold`
25
- > Scope: initial scaffold
26
+ > Status: generated by `@nakedev/go-scaffold`; refreshes as optional features are added
27
+ > Scope: current generated project configuration
26
28
 
27
29
  ## 1. Topology
28
30
 
29
- **Decision:** Single Go binary (`cmd/api`), REST over Gin.
30
- **Rationale:** One process serves all domains; a second entry point
31
- (`cmd/worker`, `cmd/migrate`) gets its own `main.go` only once there's a real
32
- second process `main.go` has no importers, so adding one later touches no
33
- other file.
31
+ **Decision:** `cmd/api` serves REST over Gin. Optional operational binaries
32
+ are separate entry points: `cmd/worker` for queued jobs and `cmd/seed` for
33
+ one-shot bootstrap tasks when those features are installed.
34
+ **Rationale:** The HTTP process stays independently deployable while worker and
35
+ seed lifecycles remain explicit. Adding either binary does not turn an API
36
+ handler or feature composition root into a process-wide god object.
34
37
 
35
38
  ## 2. Package Layout
36
39
 
37
40
  ```text
38
41
  cmd/
39
- └── api/main.go # config, slog, graceful shutdown, wires domains
42
+ └── api/
43
+ ├── main.go # process entrypoint and exit code
44
+ └── wiring.go # config, infrastructure, graceful shutdown, routes
40
45
  internal/
41
- ├── platform/ # talks to real external systems (DB, later: cache, queue, mail, ...)
42
- │ └── database/
46
+ ├── composition/ # process-level cross-feature adapters
47
+ ├── platform/ # talks to real external systems
48
+ │ └── database/ # PostgreSQL connection and pool
43
49
  ├── shared/ # pure logic/framework glue, no I/O
44
50
  │ ├── config/ apperror/ dberr/ httpx/ id/
45
51
  │ └── middleware/ pagination/ tx/
46
52
  └── app/ # domain packages (one per feature)
47
53
  └── <domain>/
54
+ ├── domain/ # business entities and errors
55
+ ├── ports/ # consumer-owned application dependencies
56
+ ├── application/ # service OR commands + queries
57
+ ├── adapters/inbound/http/ # request/response DTOs + HTTP adapter
58
+ ├── adapters/outbound/postgres/
59
+ └── composition.go # feature-local object graph
48
60
  ```
49
61
 
62
+ Installed process and platform additions:
63
+ {{#if auth}}- `cmd/seed` and `platform/authprovider/google/` for user bootstrap and Google OAuth/OIDC
64
+ {{/if}}{{#if worker}}- `cmd/worker` and `platform/{mail,queue}/` for queue/SMTP jobs{{#if (eq queue "asynq")}}, plus `platform/cache/` for Redis{{/if}}
65
+ {{/if}}{{#if observability}}- `platform/telemetry/` for the OTLP exporter
66
+ {{/if}}{{#if rbac}}- `shared/middleware/authz.go` and `app/role/` for RBAC policy
67
+ {{/if}}
68
+
50
69
  **Decision:** `platform/` vs `shared/` is split by whether the code talks to
51
70
  a real external system.
52
71
  **Rationale:** Code that does I/O against Postgres/cache/queue/SMTP/S3 →
@@ -54,12 +73,19 @@ a real external system.
54
73
  add a `shared/utils` or similarly generic package — name it after what it
55
74
  actually does.
56
75
 
76
+ Optional feature boundaries are explicit: auth owns user authentication and
77
+ provider ports; RBAC owns role policy and authorization; worker owns queue and
78
+ mail adapters; observability owns metrics/tracing setup. They do not create a
79
+ second domain architecture.
80
+
57
81
  ## 3. API Style
58
82
 
59
83
  **Decision:** REST over Gin, one handler package per domain, every route
60
84
  grouped under{{#if apiPrefix}} `/{{apiPrefix}}`{{else}} no prefix{{/if}}.
61
- **Rationale:** `handlerservicerepository`, one direction, all three
62
- layers in the same package — no cross-domain imports. The prefix is a
85
+ **Rationale:** `inbound HTTP adapter application boundary domain + ports
86
+ outbound adapter`, one direction,
87
+ with delivery, application, and persistence concerns kept inside the feature
88
+ boundary — no cross-domain imports. The prefix is a
63
89
  single project-wide choice made at `create` time (`--api-prefix`) — there is
64
90
  no per-domain versioning; a domain that needs a real breaking change gets a
65
91
  new domain package (or a new field on the existing DTO), not a duplicated
@@ -80,10 +106,16 @@ from empty command/query wrappers, so it keeps one service until its business
80
106
  needs justify the split. CQRS here does not require a second database, broker,
81
107
  or event-sourcing runtime.
82
108
 
83
- When enabled, `commands.go` owns state-changing application use cases,
84
- `queries.go` owns read use cases, and `composition.go` constructs both. The
85
- HTTP handler is still an inbound adapter and `cmd/api/wiring.go` still only
86
- selects infrastructure and registers the feature.
109
+ When enabled, `application/commands.go` owns state-changing application use
110
+ cases, `application/queries.go` owns read use cases, and `composition.go`
111
+ constructs both. The HTTP adapter is still an inbound adapter and
112
+ `cmd/api/wiring.go` still selects infrastructure, invokes process-level
113
+ adapters from `internal/composition/`, and registers the feature. A service
114
+ module instead has one `application/service.go`; these
115
+ styles are exclusive inside a module but can coexist across the monolith.
116
+
117
+ The CQRS inbound adapter is wired through `NewHandler` with separate command
118
+ and query ports. It does not generate an `application/service.go` facade.
87
119
 
88
120
  ## 5. Response and Error Model
89
121
 
@@ -92,7 +124,7 @@ selects infrastructure and registers the feature.
92
124
  **Rationale:** Handlers only ever do `c.Error(err); return` — no `c.JSON` per
93
125
  call site, no error-shape drift between domains.
94
126
 
95
- - Domain-specific errors live in each package's `errors.go` (e.g.
127
+ - Domain-specific errors live in each module's `domain/errors.go` (e.g.
96
128
  `USER_NOT_FOUND`, not a bare `NOT_FOUND`)
97
129
  - DB errors are classified once in `shared/dberr` (`IsDuplicate`,
98
130
  `IsForeignKey`) and mapped to the right HTTP status per domain
@@ -169,5 +201,11 @@ skipped by the `os.Exit` calls already in there.
169
201
 
170
202
  - `go-scaffold generate module <name>` adds a new domain package and
171
203
  wires it into `cmd/api/wiring.go`
172
- - This document only reflects the initial scaffold — update it as the real
173
- architecture evolves
204
+ - `go-scaffold check` validates the physical split, process-composition
205
+ boundary, layer dependencies, and exclusive service/CQRS choice for every
206
+ discovered module
207
+ - `go-scaffold add auth`, `add worker`, `add rbac`, and `add observability`
208
+ refresh this document while it still matches the generated version. A
209
+ hand-edited document is preserved and must be updated by its owner.
210
+ - This document describes package boundaries, not completed business behavior;
211
+ generated methods remain explicit TODO/501 stubs until implemented.
@@ -1,7 +1,7 @@
1
1
  # Coding Patterns: {{projectName}}
2
2
 
3
- > Status: generated by `@nakedev/go-scaffold`
4
- > Last updated: scaffold creation time
3
+ > Status: generated by `@nakedev/go-scaffold`; current generated-project conventions
4
+ > Last reviewed: generated at project creation and refreshed with feature docs
5
5
 
6
6
  ## Domain Package Shape
7
7
 
@@ -10,44 +10,56 @@ like this — copy the shape by hand if you ever add one without the CLI:
10
10
 
11
11
  ```text
12
12
  internal/app/<domain>/
13
- ├── model/ # domain model(s) + GORM table(s) — a folder, one file per table
14
- └── model.go
15
- ├── dto.go # request/response structs + mapping (never leak the model directly)
16
- ├── errors.go # domain error catalog (<DOMAIN>_NOT_FOUND, ...)
17
- ├── repository.go # the only place that touches the DB for this domain, every method takes ctx
18
- ├── service.go # business logic; declares the repository interface it needs (mockable in tests)
19
- ├── commands.go # optional: command port + state-changing application handlers (`--cqrs`)
20
- ├── queries.go # optional: query port + read-only application handlers (`--cqrs`)
21
- ├── composition.go # feature-local repository service → handler wiring
22
- ├── handler.go # HTTP: routing, bind, delegate, respond
23
- ├── service_test.go # unit test, function-backed repository stub, no DB
24
- ├── handler_test.go # HTTP unit test, service stub, no DB
25
- └── repository_test.go # Postgres integration test against migrated schema
13
+ ├── domain/
14
+ ├── entity.go # business state and invariants; no Gin/GORM/HTTP
15
+ │ └── errors.go # domain error catalog (<DOMAIN>_NOT_FOUND, ...)
16
+ ├── ports/
17
+ │ └── repository.go # consumer-owned persistence/application interfaces
18
+ ├── application/
19
+ ├── dto.go # transport-neutral use-case input/output types
20
+ ├── service.go # service-style use-case orchestration
21
+ ├── commands.go # CQRS writes, when selected
22
+ ├── queries.go # CQRS reads, when selected
23
+ │ └── *_test.go # application unit tests, no DB
24
+ ├── adapters/
25
+ │ ├── inbound/http/
26
+ │ │ ├── dto.go # JSON/binding DTOs and application/HTTP mapping
27
+ │ │ ├── handler.go # Gin routing, error/response mapping
28
+ │ │ └── handler_test.go # HTTP adapter tests, no DB
29
+ │ └── outbound/postgres/
30
+ │ ├── model.go # persistence model + explicit domain mapping
31
+ │ ├── repository.go # GORM adapter; the only DB access in the module
32
+ │ └── repository_test.go # Postgres integration tests
33
+ └── composition.go # feature-local adapter → application → handler wiring
26
34
  ```
27
35
 
28
- ### Model
29
-
30
- - Lives in its own `model` subpackage (`internal/app/<domain>/model`), imported
31
- by the rest of the domain as `model.<Type>`
32
- - A folder, not a single file, so a domain with more than one table (e.g.
33
- `order` + `order_item`) adds one file per table instead of growing a single
34
- file — `generate module` only ever creates the first one
35
- - Every consumer inside `cmd/api/wiring.go` imports each domain's `model`
36
- package under an alias (`ordermodel`, `usermodel`, ...) since they all share
37
- the package name `model`
36
+ `service.go` and `commands.go`/`queries.go` are mutually exclusive inside a
37
+ module. Service and CQRS can coexist across different modules in the same
38
+ modular monolith. The root package contains only `composition.go`; other code
39
+ must import the specific boundary package it belongs to.
40
+
41
+ ### Domain and persistence model
42
+
43
+ - `domain/entity.go` owns business state and invariants. It must not import
44
+ Gin, GORM, SQL drivers, Redis, HTTP, or environment configuration.
45
+ - `adapters/outbound/postgres/model.go` owns persistence-only fields and
46
+ explicit `toDomain`/`fromDomain` mapping. A database column or GORM tag must
47
+ not silently become part of the business API.
48
+ - A domain with more than one table should add one persistence model per table
49
+ in the outbound adapter, with the corresponding domain type or value object
50
+ kept in `domain/`.
38
51
  - `TableName()` returns a schema-qualified name (`order_svc.orders`, not
39
52
  `orders`) — every domain gets its own Postgres schema, created by its own
40
53
  migration (`CREATE SCHEMA IF NOT EXISTS`) and by `cmd/api/wiring.go` before
41
- the development table bootstrap runs (the bootstrap creates tables, never
42
- the schema they live in). A cross-domain FK is still fine see the FK rules below this
43
- only stops one domain's table from silently colliding with another's, or a
44
- raw SQL `JOIN` from reaching into a domain it doesn't own without at least
45
- naming the schema it's crossing into.
54
+ the development table bootstrap runs. A cross-domain FK is still fine — see
55
+ the FK rules belowthis only stops one domain's table from silently
56
+ colliding with another's.
46
57
 
47
58
  ## Layer Conventions
48
59
 
49
60
  ### Handler
50
- - Owns routing, request binding, and calling the service
61
+ - Owns routing, request binding, and calling the application service or
62
+ command/query port
51
63
  - No business logic — `c.Error(err); return` on failure, nothing more
52
64
  - Reads pagination via `pagination.Parse(c)`, wraps list responses with
53
65
  `p.Response(out)`
@@ -64,22 +76,26 @@ selects the matching port for each route. They may share the same
64
76
  repository/database in this modular monolith — separate storage or event
65
77
  sourcing is not implied.
66
78
 
67
- The generated `Service` remains a compatibility facade for tests and existing
68
- callers. New feature code should depend on the narrower command/query port it
69
- actually needs. `generate module --defaults` uses the project default; choose
70
- the single service path for simple CRUD and CQRS only when the feature earns
71
- the extra boundary.
79
+ The generated CQRS application has separate `CommandPort` and `QueryPort`
80
+ interfaces and separate handlers. Its HTTP adapter receives both through
81
+ `NewHandler`; there is no generated `application/service.go` facade to hide the
82
+ split. New feature code should depend on the narrower port it actually needs.
83
+ `generate module --defaults` uses the project default; choose the single service
84
+ path for simple CRUD and CQRS only when the feature earns the extra boundary.
72
85
 
73
86
  ### Service
74
87
  - Contains the business logic, knows nothing about HTTP
75
- - Declares a `repository` interface for what it needs from the data layer —
76
- this is what makes it mockable in `service_test.go` without a DB
88
+ - Depends on consumer-owned interfaces from `ports/` for what it needs from
89
+ the data layer — this is what makes it mockable in `application/*_test.go`
90
+ without a DB
77
91
  - Generates the ID itself via `id.New()` before calling `repository.Create`
78
- - Maps DB errors to domain errors with `dberr.IsDuplicate` /
79
- `dberr.IsForeignKey`, never lets a raw DB error escape
92
+ - Returns domain errors and never constructs an HTTP response. The outbound
93
+ adapter maps driver errors with `dberr.IsDuplicate` /
94
+ `dberr.IsForeignKey` before they reach the application boundary.
80
95
 
81
- ### Repository
82
- - The only file per domain that talks to GORM
96
+ ### Repository adapter
97
+ - `adapters/outbound/postgres/repository.go` is the only module code that talks
98
+ to GORM
83
99
  - Every method takes `ctx context.Context` first, so a cancelled request
84
100
  cancels the query
85
101
  - Every query starts from `tx.From(ctx, r.db).WithContext(ctx)`, never from
@@ -95,16 +111,18 @@ the extra boundary.
95
111
  and GORM can misinterpret a bare struct arg
96
112
 
97
113
  ### DTOs
98
- - `createInput`/`updateInput` (request, `binding:` tags) and `response`
99
- (what's actually sent back) are separate types from the model — adding a
100
- DB column later doesn't leak it to clients until you decide to
114
+ - `application/dto.go` owns transport-neutral use-case inputs and output read
115
+ models. `adapters/inbound/http/dto.go` owns JSON names, binding/validation
116
+ tags, and explicit request/response mapping at the HTTP boundary.
117
+ - When `generate method` adds a POST input, it adds both the application input
118
+ and an inbound HTTP DTO/mapping seam. Fill both deliberately; do not bind
119
+ Gin JSON directly into an application type.
101
120
 
102
121
  ### Error Catalog
103
- - One function per error, not a shared `var` the error middleware writes
104
- the request ID onto the returned pointer, so a shared instance would race
105
- across concurrent requests
106
- - Codes are domain-specific (`ORDER_NOT_FOUND`), never the generic
107
- `apperror.NewNotFound()` directly from a handler
122
+ - Domain sentinels live in `domain/errors.go`; they are values used with
123
+ `errors.Is`, not HTTP errors or mutable request state.
124
+ - The inbound adapter maps those sentinels to domain-specific HTTP codes
125
+ (`ORDER_NOT_FOUND`); domain/application code never constructs an HTTP error.
108
126
 
109
127
  ## Optimistic Locking
110
128
 
@@ -123,13 +141,13 @@ and `response` echoes it back: the client round-trips the value, and an
123
141
  update that arrives without one is rejected rather than treated as a blind
124
142
  overwrite.
125
143
 
126
- In a CRUD skeleton the service translates `ErrStaleVersion` into `errStale()`
127
- from the domain's error catalog (`<DOMAIN>_STALE`, HTTP 409) a lost update
128
- is a client problem to retry, not a 500. A minimal module has the same
129
- `Repository.Update` and the same `errStale()` waiting for it, just no update
130
- path yet. Keep the chain intact when you add one by hand:
131
- `errors.Is(err, ErrStaleVersion)` `errStale()`, and never set `Version` on
132
- the model yourself — `Repository.Update` owns the bump.
144
+ The inbound adapter maps `ErrStaleVersion` to the domain's `<DOMAIN>_STALE`
145
+ HTTP 409 response a lost update is a client problem to retry, not a 500. A
146
+ minimal module has the same `Repository.Update` guard waiting for its first
147
+ update use case. Keep the chain intact when you add one by hand:
148
+ `errors.Is(err, domain.ErrStaleVersion)` adapter HTTP mapping, and never set
149
+ `Version` on the persistence model yourself `Repository.Update` owns the
150
+ bump.
133
151
 
134
152
  ## Domains With a Foreign Key (Relations) — 3 Rules
135
153
 
@@ -143,18 +161,19 @@ if this project has one):
143
161
  2. **Declare the FK constraint in migration SQL**
144
162
  (`REFERENCES ... ON DELETE ...`), not a GORM tag — the development bootstrap
145
163
  doesn't create the constraint, which would make dev and prod schemas diverge.
146
- 3. **Map the FK error to the right status** via `dberr.IsForeignKey`
147
- inserting a reference to a missing parent, or deleting a parent that
148
- still has children, is a client error (409/422), not a 500. Don't
149
- pre-check existence before insert; let the DB enforce it atomically and
150
- catch the error — a pre-check has a TOCTOU race.
164
+ 3. **Map the FK error to the right status** in the outbound adapter with
165
+ `dberr.IsForeignKey`, then let the inbound adapter render it inserting a
166
+ reference to a missing parent, or deleting a parent that still has children,
167
+ is a client error (409/422), not a 500. Don't pre-check existence before
168
+ insert; let the DB enforce it atomically and catch the error — a pre-check
169
+ has a TOCTOU race.
151
170
 
152
171
  ## Calling Another Domain's Logic
153
172
 
154
173
  For behavior, not just a data reference (e.g. `order` needs `user`'s email
155
- to put on a receipt) — the caller's `service.go` declares its own narrow
156
- interface for exactly what it needs, the same way it already declares a
157
- `repository` interface.
174
+ to put on a receipt) — the caller's application boundary (`application/service.go`,
175
+ `application/commands.go`, or `application/queries.go`) declares its own narrow interface for exactly
176
+ what it needs, the same way it already declares a `repository` interface.
158
177
 
159
178
  **The interface speaks in the caller's own terms.** No domain package ever
160
179
  imports another domain package — `golangci-lint`'s `depguard` rules enforce
@@ -162,15 +181,16 @@ this, and it is the same rule that makes rule 1 above work. So the interface
162
181
  names primitives, or types the caller owns, never `user.Response`:
163
182
 
164
183
  ```go
165
- // order/service.go
184
+ // order/application/service.go
166
185
  type userLookup interface {
167
186
  EmailOf(ctx context.Context, id uuid.UUID) (string, error)
168
187
  }
169
188
  ```
170
189
 
171
- The feature keeps its own repository/service/handler composition in
172
- `composition.go`; `cmd/api/wiring.go` supplies the DB and any cross-feature
173
- security dependencies. For example, auth and RBAC are composed locally and
190
+ The feature keeps its own repository/application/handler composition in
191
+ `composition.go`; `cmd/api/wiring.go` supplies the DB and registers routes.
192
+ Process-level adapters for cross-feature capabilities live in
193
+ `internal/composition/`. For example, auth and RBAC are composed locally and
174
194
  the root passes only the role feature's public capabilities:
175
195
 
176
196
  ```go
@@ -181,12 +201,13 @@ user.NewHandlerFromDB(
181
201
  ```
182
202
 
183
203
  For ordinary cross-domain behaviour, the caller still declares a narrow
184
- interface next to its application service. Its local composition accepts that
185
- port, while the root supplies an adapter; the root never reaches into the
186
- callee's repository or handler. A func-to-interface shim is one small option:
204
+ interface next to its application boundary. Its local composition accepts that
205
+ port, while `internal/composition/` supplies an adapter; the process root never
206
+ reaches into the callee's repository or handler. A func-to-interface shim is
207
+ one small option:
187
208
 
188
209
  ```go
189
- // order/service.go
210
+ // order/application/service.go
190
211
  type UserLookupFunc func(ctx context.Context, id uuid.UUID) (string, error)
191
212
 
192
213
  func (f UserLookupFunc) EmailOf(ctx context.Context, id uuid.UUID) (string, error) {
@@ -201,6 +222,10 @@ can never silently ripple into `order`. The adapter is also the honest place
201
222
  to notice you are reaching for something that should have been the caller's
202
223
  own data.
203
224
 
225
+ Cross-feature integration tests belong under `internal/integration`, where
226
+ they may compose real feature services and adapters. A feature's own unit and
227
+ repository tests stay inside its package and must not import another feature.
228
+
204
229
  - **One direction only.** If `user` would need to call back into `order`,
205
230
  don't wire it both ways — either the two belong in one domain, or the
206
231
  callback needs an event/queue, not a direct call.
@@ -216,31 +241,33 @@ own data.
216
241
  go-scaffold generate method <domain> <name> --type <get|post|put|patch|delete> [--get-mode all|one] [--field <name>]
217
242
  ```
218
243
 
219
- Patches `handler.go`/`service.go` (and `commands.go` or `queries.go` for a
220
- module generated with `--cqrs`; plus `repository.go` + the relevant repository
221
- port + its `repositoryStub` test stub, for a `get --get-mode one --field`
222
- lookup) in place, at the `// go-scaffold:*` marker comments near the end of
223
- each file. **Don't delete those markers** — they're where the next
224
- `generate method` call inserts. The method body is always left as a `TODO`
225
- that compiles and returns a clean `500` (`apperror.NewInternal(cause)`) rather
226
- than guessing at business logic — same spirit as `generate module`'s
227
- placeholder fields.
244
+ Patches `adapters/inbound/http/handler.go`, its request DTO seam, and the application boundary (or
245
+ `application/commands.go`/`application/queries.go` for a CQRS module; plus
246
+ `ports/repository.go` and the outbound repository adapter/test seam for a
247
+ `get --get-mode one --field` lookup) in place, at the
248
+ `// go-scaffold:*` marker comments near the end of each file. **Don't delete
249
+ those markers** — they're where the next `generate method` call inserts. The
250
+ method body is always left as a compiling `TODO` rather than guessing at
251
+ business logic — same spirit as `generate module`'s placeholder fields.
228
252
 
229
253
  ## Testing Conventions
230
254
 
231
255
  - Unit and integration tests live in the same directory as the code under
232
256
  test (Go convention) — never a separate `test/` folder. `test/` is only
233
257
  for e2e black-box suites or fixtures.
234
- - `service_test.go` uses a function-backed repository stub so each dependency
235
- method has independent behavior and argument assertions.
236
- - `handler_test.go` uses a service stub — HTTP tests never need Postgres.
237
- - `repository_test.go` runs against a migrated Postgres database inside a
258
+ - `application/service_test.go` uses a function-backed repository stub so each
259
+ dependency method has independent behavior and argument assertions.
260
+ - `adapters/inbound/http/handler_test.go` checks route composition; focused
261
+ HTTP tests can inject an application port and never need Postgres.
262
+ - `adapters/outbound/postgres/repository_test.go` runs against a migrated
263
+ Postgres database inside a
238
264
  transaction that's rolled back after each test. Local runs may skip when
239
265
  `TEST_DB_DSN` is unset; CI sets `REQUIRE_TEST_DB=true` so it must run.
240
266
 
241
267
  ## Docs Maintenance
242
268
 
243
- - This document describes the output produced by `go-scaffold create` for
244
- the current configuration
245
- - If new domains are generated or the architecture changes, update this doc
246
- to match the real project
269
+ - This document describes the conventions used by generated domains and the
270
+ canonical split emitted by `go-scaffold`.
271
+ - When a shared generator contract changes, update the templates and add a
272
+ regression test. Generated projects with hand-edited docs are left alone by
273
+ incremental feature refreshes and must be updated by their owners.
@@ -1,6 +1,6 @@
1
1
  # Tech Stack: {{projectName}}
2
2
 
3
- > Status: generated by `@nakedev/go-scaffold`
3
+ > Status: generated by `@nakedev/go-scaffold`; refreshes as optional features are added
4
4
 
5
5
  ## Stack Overview
6
6
 
@@ -15,6 +15,9 @@
15
15
  | Logging | `log/slog`, JSON handler |
16
16
  | Migrations | [golang-migrate](https://github.com/golang-migrate/migrate) |
17
17
  | Testing | stdlib `testing`, real Postgres for integration tests |
18
+ | Authentication | {{#if auth}}JWT access/refresh sessions, password recovery, email verification, provider login, and MFA{{else}}optional via `go-scaffold add auth`{{/if}} |
19
+ | Authorization | {{#if rbac}}RBAC role/permission middleware{{else}}optional RBAC via `go-scaffold add rbac`{{/if}} |
20
+ | Background jobs | {{#if worker}}{{queue}} queue backend with SMTP mail and `cmd/worker`{{else}}optional via `go-scaffold add worker`{{/if}} |
18
21
 
19
22
  ## Scaffolded Capabilities
20
23
 
@@ -24,6 +27,9 @@
24
27
  - Docker Compose (local Postgres): `{{#if docker}}enabled{{else}}disabled{{/if}}`
25
28
  - OpenAPI docs (`docs/openapi.yaml`, working-copy only — never served over HTTP): `{{#if openapiDocs}}enabled{{else}}disabled{{/if}}`
26
29
  - Metrics + tracing (Prometheus `/metrics`, OpenTelemetry for Gin + GORM, `cmd/api` only): `{{#if observability}}enabled{{else}}disabled{{/if}}`
30
+ - Authentication and browser OAuth: `{{#if auth}}enabled{{else}}disabled{{/if}}`
31
+ - Background worker and mail: `{{#if worker}}enabled{{else}}disabled{{/if}}`
32
+ - RBAC roles, permissions, and authorization: `{{#if rbac}}enabled{{else}}disabled{{/if}}`
27
33
  - API route prefix: `{{#if apiPrefix}}/{{apiPrefix}}{{else}}(none){{/if}}`
28
34
  - CI (`.github/workflows/ci.yml` — build, vet, gofmt check, golangci-lint, `go test` with a real Postgres service): always enabled
29
35
 
@@ -31,8 +37,14 @@
31
37
 
32
38
  {{#if docker}}- Local Postgres comes from `docker-compose.yml`
33
39
  {{else}}- No Docker scaffolding included — bring your own Postgres and point `DB_DSN` at it
34
- {{/if}}- `APP_ENV=development` enables the convenience table bootstrap;
40
+ {{/if}}{{#if worker}}{{#if (eq queue "river")}}- Run `make river-migrate` before relying on queued jobs in a fresh database;
41
+ run `make river-migrate-test` as well when `REQUIRE_TEST_DB=true` runs the
42
+ River worker round-trip test.{{/if}}{{/if}}
43
+ - `APP_ENV=development` enables the convenience table bootstrap;
35
44
  `APP_ENV=production` requires `migrate up` as a deploy step instead
45
+ {{#if worker}}{{#if (eq queue "river")}}- River uses the database/sql driver and polls for jobs instead of receiving
46
+ PostgreSQL LISTEN/NOTIFY wakeups; this is deliberate for a shared GORM pool.
47
+ {{/if}}{{/if}}
36
48
  {{#if openapiDocs}}- `docs/openapi.yaml` is hand-written, not generated — update it whenever an
37
49
  endpoint or DTO changes
38
50
  {{/if}}{{#if observability}}- `OTEL_EXPORTER_OTLP_ENDPOINT` unset (dev default) means tracing exports
@@ -44,3 +56,6 @@
44
56
  resolves them to the versions that feature was built against instead of
45
57
  whatever happened to be newest that day. The full list is `go.mod`; this
46
58
  table only covers what `create` itself brings in
59
+ - Auth, worker, and RBAC are opt-in. Their `add` commands update this snapshot
60
+ when the generated file is untouched; hand-edited docs are left alone and
61
+ require a manual update.
@@ -0,0 +1,45 @@
1
+ package httpadapter
2
+
3
+ import (
4
+ "time"
5
+
6
+ "{{goModule}}/internal/app/{{modulePath}}/application"
7
+
8
+ "github.com/google/uuid"
9
+ )
10
+
11
+ // response is the HTTP representation of the application output. Keep JSON
12
+ // names and protocol-only fields at this inbound boundary.
13
+ type response struct {
14
+ ID uuid.UUID `json:"id"`
15
+ CreatedAt time.Time `json:"created_at"`
16
+ Version int `json:"version"`
17
+ }
18
+
19
+ func toResponse(out application.Response) response {
20
+ return response{ID: out.ID, CreatedAt: out.CreatedAt, Version: out.Version}
21
+ }
22
+
23
+ // Request DTOs belong to the inbound adapter. Keep JSON names, validation
24
+ // tags, and request-to-command mapping here; application inputs stay usable
25
+ // from jobs, tests, and other non-HTTP callers.
26
+ type createInput struct {
27
+ // TODO: mirror the request fields added to application.CreateInput.
28
+ }
29
+
30
+ type updateInput struct {
31
+ Version int `json:"version" binding:"required,min=1"`
32
+ // TODO: mirror the update fields added to application.UpdateInput.
33
+ }
34
+
35
+ func toCreateInput(in createInput) application.CreateInput {
36
+ // TODO: map request fields explicitly as the application contract evolves.
37
+ _ = in
38
+ return application.CreateInput{}
39
+ }
40
+
41
+ func toUpdateInput(in updateInput) application.UpdateInput {
42
+ return application.UpdateInput{Version: in.Version}
43
+ }
44
+
45
+ // go-scaffold:dto
@@ -0,0 +1,28 @@
1
+ package httpadapter
2
+
3
+ import (
4
+ "time"
5
+
6
+ "{{goModule}}/internal/app/{{modulePath}}/application"
7
+
8
+ "github.com/google/uuid"
9
+ )
10
+
11
+ // response is the HTTP representation of the application output. Keep JSON
12
+ // names and protocol-only fields at this inbound boundary.
13
+ //nolint:unused // generate method uses this seam when the first endpoint is added.
14
+ type response struct {
15
+ ID uuid.UUID `json:"id"`
16
+ CreatedAt time.Time `json:"created_at"`
17
+ Version int `json:"version"`
18
+ }
19
+
20
+ //nolint:unused // generate method uses this seam when the first endpoint is added.
21
+ func toResponse(out application.Response) response {
22
+ return response{ID: out.ID, CreatedAt: out.CreatedAt, Version: out.Version}
23
+ }
24
+
25
+ // A lean module has no HTTP body endpoints yet. The marker gives
26
+ // `generate method` a stable place to add request DTOs when a POST is earned.
27
+
28
+ // go-scaffold:dto