@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
@@ -1,14 +1,6 @@
1
- -- if an admin created a custom role after this migration and it's still
2
- -- assigned to any user, dropping the roles table would orphan that value —
3
- -- raise a clear, actionable error instead of a raw FK-violation.
4
- DO $$
5
- BEGIN
6
- IF EXISTS (SELECT 1 FROM user_svc.users WHERE role NOT IN ('staff', 'admin')) THEN
7
- RAISE EXCEPTION 'cannot roll back add_roles: users still have a role other than staff/admin — reassign them to staff/admin first';
8
- END IF;
9
- END $$;
10
-
11
- ALTER TABLE user_svc.users DROP COLUMN role;
1
+ -- Auth owns users.role, so RBAC removes only the constraint it added. The
2
+ -- column and existing assignments remain valid for auth-only operation.
3
+ ALTER TABLE user_svc.users DROP CONSTRAINT IF EXISTS users_role_fkey;
12
4
 
13
5
  DROP TABLE role_svc.role_permissions;
14
6
  DROP TABLE role_svc.permissions;
@@ -33,9 +33,20 @@ INSERT INTO role_svc.permissions (code, description) VALUES
33
33
  INSERT INTO role_svc.role_permissions (role_code, permission_code)
34
34
  SELECT 'admin', code FROM role_svc.permissions;
35
35
 
36
- -- cross-schema FK: role_svc's migration is the one place allowed to alter
37
- -- user_svc's table, because this feature's whole point is "give users a
38
- -- role" see docs/architect/patterns.md's FK rules for why a plain FK, not
39
- -- an app-level existence check, is the right way to enforce this.
40
- ALTER TABLE user_svc.users
41
- ADD COLUMN role VARCHAR(20) NOT NULL DEFAULT 'staff' REFERENCES role_svc.roles(code);
36
+ -- Auth owns the stable users.role column. RBAC owns the role catalog and adds
37
+ -- only the cross-schema constraint after that catalog exists. This keeps each
38
+ -- migration aligned with its module instead of making RBAC recreate auth's
39
+ -- persistence shape.
40
+ DO $$
41
+ BEGIN
42
+ IF NOT EXISTS (
43
+ SELECT 1
44
+ FROM pg_constraint
45
+ WHERE conname = 'users_role_fkey'
46
+ AND conrelid = 'user_svc.users'::regclass
47
+ ) THEN
48
+ ALTER TABLE user_svc.users
49
+ ADD CONSTRAINT users_role_fkey
50
+ FOREIGN KEY (role) REFERENCES role_svc.roles(code);
51
+ END IF;
52
+ END $$;
@@ -0,0 +1,84 @@
1
+ package queue
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "os"
7
+ "testing"
8
+ "time"
9
+
10
+ "github.com/google/uuid"
11
+ "gorm.io/driver/postgres"
12
+ "gorm.io/gorm"
13
+ )
14
+
15
+ type riverRoundTripJob struct {
16
+ Marker string `json:"marker"`
17
+ }
18
+
19
+ func (riverRoundTripJob) Kind() string { return "queue-test-round-trip" }
20
+
21
+ // TestRiverEnqueueAndWorkerProcess proves the Postgres queue path beyond
22
+ // client construction: a job is written, picked up by the worker adapter,
23
+ // decoded, and handed to the registered backend-neutral handler.
24
+ func TestRiverEnqueueAndWorkerProcess(t *testing.T) {
25
+ dsn := os.Getenv("TEST_DB_DSN")
26
+ if dsn == "" {
27
+ if os.Getenv("REQUIRE_TEST_DB") == "true" {
28
+ t.Fatal("TEST_DB_DSN is required when REQUIRE_TEST_DB=true")
29
+ }
30
+ t.Skip("River integration test skipped: set TEST_DB_DSN to a database with River migrations")
31
+ }
32
+
33
+ db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
34
+ if err != nil {
35
+ t.Fatalf("open test database: %v", err)
36
+ }
37
+ sqlDB, err := db.DB()
38
+ if err != nil {
39
+ t.Fatalf("get test database handle: %v", err)
40
+ }
41
+ t.Cleanup(func() { _ = sqlDB.Close() })
42
+
43
+ worker, err := NewRiverWorker(db, 1)
44
+ if err != nil {
45
+ t.Fatalf("create River worker: %v", err)
46
+ }
47
+
48
+ received := make(chan string, 1)
49
+ worker.Handle((riverRoundTripJob{}).Kind(), func(_ context.Context, raw []byte) error {
50
+ var job riverRoundTripJob
51
+ if err := json.Unmarshal(raw, &job); err != nil {
52
+ return err
53
+ }
54
+ received <- job.Marker
55
+ return nil
56
+ })
57
+
58
+ workerCtx, cancel := context.WithCancel(context.Background())
59
+ t.Cleanup(func() {
60
+ cancel()
61
+ stopCtx, stopCancel := context.WithTimeout(context.Background(), 5*time.Second)
62
+ defer stopCancel()
63
+ if err := worker.Stop(stopCtx); err != nil {
64
+ t.Errorf("stop River worker: %v", err)
65
+ }
66
+ })
67
+ if err := worker.Start(workerCtx); err != nil {
68
+ t.Fatalf("start River worker: %v", err)
69
+ }
70
+
71
+ expected := uuid.NewString()
72
+ if err := worker.Enqueue(context.Background(), riverRoundTripJob{Marker: expected}, nil); err != nil {
73
+ t.Fatalf("enqueue River job: %v", err)
74
+ }
75
+
76
+ select {
77
+ case actual := <-received:
78
+ if actual != expected {
79
+ t.Fatalf("worker received marker %q, want %q", actual, expected)
80
+ }
81
+ case <-time.After(10 * time.Second):
82
+ t.Fatal("River worker did not process the enqueued job within 10s")
83
+ }
84
+ }
@@ -76,6 +76,7 @@ Run from the project root and pass every value as a flag:
76
76
  go-scaffold generate module <name> [--profile <lean|crud|cqrs>] [--defaults] [--auth] [--permission <code>]
77
77
  go-scaffold generate method <module> <name> --type <get|post|put|patch|delete> [--get-mode all|one] [--field <name>]
78
78
  go-scaffold generate migration <name>
79
+ go-scaffold check
79
80
  go-scaffold config
80
81
  go-scaffold add auth --store <postgres|redis> --browser-topology <same-origin|same-site|cross-site> [--defaults] [--yes]
81
82
  go-scaffold add worker --queue <postgres|redis> [--defaults]
@@ -116,10 +117,10 @@ corresponding features installed. There is no final confirmation for module
116
117
  generation; review the profile and flags before running it.
117
118
 
118
119
  `go-scaffold config` changes defaults for future modules only. Existing modules
119
- are not rewritten. `config show` and `config validate` are intentionally
120
- read-only and do not open a wizard. The bare `generate` command first asks
121
- whether to generate a module, method, or migration; the bare `add` command
122
- first asks which infrastructure feature to add.
120
+ are not rewritten. `check`, `config show`, and `config validate` are
121
+ intentionally read-only and do not open a wizard. The bare `generate` command
122
+ first asks whether to generate a module, method, or migration; the bare `add`
123
+ command first asks which infrastructure feature to add.
123
124
 
124
125
  ## Generated module contract
125
126
 
@@ -128,34 +129,41 @@ defaults. On a fresh project that is a Lean module with:
128
129
 
129
130
  ~~~text
130
131
  internal/app/<pkg>/
131
- model/model.go
132
- dto.go
133
- errors.go
134
- repository.go
135
- service.go
132
+ domain/entity.go
133
+ domain/errors.go
134
+ ports/repository.go
135
+ application/dto.go
136
+ application/service.go
137
+ application/service_test.go
138
+ adapters/inbound/http/dto.go
139
+ adapters/inbound/http/handler.go
140
+ adapters/inbound/http/handler_test.go
141
+ adapters/outbound/postgres/model.go
142
+ adapters/outbound/postgres/repository.go
143
+ adapters/outbound/postgres/repository_test.go
136
144
  composition.go
137
- handler.go
138
- service_test.go
139
- handler_test.go
140
- repository_test.go
141
145
  migrations/<version>_create_<plural>.{up,down}.sql
142
146
  ~~~
143
147
 
144
148
  The Lean/minimal module has no endpoint yet, but includes the complete
145
149
  data-access surface and test seams needed by generate method. The CRUD profile
146
150
  adds a CRUD starter and routes list/get/create/update/delete. The CQRS profile
147
- adds separate commands.go and queries.go application handlers; Advanced can
148
- combine CRUD + CQRS, or use CQRS alone before adding methods one at a time.
151
+ replaces `application/service.go` and its test with
152
+ `application/commands.go`, `application/queries.go`, and
153
+ `application/cqrs_test.go`; Advanced can combine CRUD + CQRS, or use CQRS
154
+ alone before adding methods one at a time. Service and CQRS are mutually
155
+ exclusive within one module, but may coexist across modules.
149
156
  With OpenAPI enabled it also creates the per-domain documents and updates
150
157
  docs/openapi.yaml.
151
158
 
152
159
  Feature-local composition.go constructs repository → application handlers →
153
160
  handler. A module generated with `--cqrs` constructs separate command/query
154
161
  handlers there; a regular module constructs its service there.
155
- cmd/api/wiring.go is the process composition root: it selects shared
156
- infrastructure, supplies explicit cross-feature ports/security dependencies,
157
- and registers routes. Do not move business rules or feature-internal
158
- constructors into wiring, or hand-edit root route markers for a new endpoint.
162
+ `internal/composition/` owns process-level adapters between feature ports.
163
+ `cmd/api/wiring.go` selects shared infrastructure, invokes those adapters, and
164
+ registers routes. Do not move business rules or feature-internal constructors
165
+ into either process-level location, or hand-edit root route markers for a new
166
+ endpoint.
159
167
 
160
168
  ## Method shapes and stub safety
161
169
 
@@ -182,14 +190,19 @@ This project is modular plus Hexagonal/DDD-friendly, with CQRS available via
182
190
  the `cqrs` profile or the backwards-compatible
183
191
  `go-scaffold generate module <name> --cqrs` flag:
184
192
 
185
- - handlers are inbound adapters and know HTTP/Gin only;
186
- - services or commands.go/queries.go (and, for complex flows,
187
- internal/app/<feature>/application/) are application/use-case boundaries and
188
- depend on narrow ports;
189
- - models and domain invariants stay independent of Gin, GORM, Redis, and HTTP;
193
+ - `adapters/inbound/http/dto.go` owns JSON names, binding/validation tags, and
194
+ request/response mapping; handlers are inbound adapters and know HTTP/Gin
195
+ only;
196
+ - `application/service.go` or `application/commands.go` plus
197
+ `application/queries.go` are application/use-case boundaries and depend on
198
+ narrow ports. The service and CQRS shapes are mutually exclusive inside one
199
+ module;
200
+ - `domain/` entities and invariants stay independent of Gin, GORM, Redis, and
201
+ HTTP; persistence models belong under `adapters/outbound/postgres`;
190
202
  - repositories/token stores/mail/queue clients are outbound adapters;
191
- - composition.go wires one feature locally, while cmd/api/wiring.go wires
192
- the process.
203
+ - composition.go wires one feature locally, internal/composition/ wires
204
+ cross-feature capabilities, and cmd/api/wiring.go boots the process and
205
+ registers routes.
193
206
 
194
207
  DDD does not require every CRUD row to become a large aggregate. CQRS optional:
195
208
  use the CQRS profile when command and query models, consistency, or
@@ -199,9 +212,10 @@ service and repository port are clearer. Do not add a broker, a second
199
212
  database, or empty command/query layers just for naming.
200
213
 
201
214
  Never import another feature's private model, repository, or handler. For
202
- cross-feature behavior, define a narrow public application port, use a
203
- documented feature API, or publish an explicit event. Keep dependencies
204
- flowing inward:
215
+ cross-feature behavior, define a narrow public application port, put a
216
+ synchronous adapter in internal/composition/ (importing only the feature's
217
+ public package root), use a documented feature API, or publish an explicit
218
+ event. Keep dependencies flowing inward:
205
219
 
206
220
  ~~~text
207
221
  handler -> application/use case -> domain + ports -> adapters
@@ -209,13 +223,13 @@ handler -> application/use case -> domain + ports -> adapters
209
223
  +--- composition -+
210
224
  ~~~
211
225
 
212
- The generated auth facade is a pragmatic compatibility boundary: its service
213
- contracts currently use the feature's `model` values and shared `apperror`,
214
- while handlers own Gin and provider adapters own SDK configuration. Do not
215
- broaden that coupling by importing Gin, provider SDKs, or HTTP handlers into
216
- use cases. New complex flows should prefer transport-neutral DTOs and narrow
217
- ports; a deeper separation of the existing auth facade is an explicit
218
- architecture refactor and needs focused tests.
226
+ Auth and RBAC use the same canonical split as every other feature:
227
+ `domain/`, `application/`, `ports/`, `adapters/`, and a feature-local
228
+ `composition.go`. Auth owns user use cases and ports; RBAC owns role policy and
229
+ authorization adapters. There is no `compat/`, feature-level `model/`, or
230
+ second implementation tree. If an existing security flow needs a refactor,
231
+ split it at the application boundary and keep provider SDKs, Gin, and GORM
232
+ inside their respective adapters.
219
233
 
220
234
  ## Authentication and browser OAuth contract
221
235
 
@@ -345,6 +359,10 @@ still requires PostgreSQL even when refresh tokens use Redis. When routes or
345
359
  OpenAPI templates change, inspect the updated `docs/openapi.yaml` and run the
346
360
  configured OpenAPI linter.
347
361
 
362
+ When the project uses River, run `make river-migrate-test` after
363
+ `make migrate-up-test`. The River integration test must enqueue a real job and
364
+ observe the worker handler; a process that only boots is not enough evidence.
365
+
348
366
  Only in the `go-scaffold` source repository, run `pnpm run verify` for CLI or
349
367
  template changes, then generate a fresh sample project and inspect both its
350
368
  source and guidance files. A generated Go project does not contain the
@@ -15,8 +15,8 @@ linters:
15
15
  depguard:
16
16
  # What keeps this a modular monolith rather than a pile of folders: a
17
17
  # domain may not reach into another domain's package. It declares a
18
- # narrow interface for what it needs and cmd/api/wiring.go supplies the
19
- # concrete service — see docs/architect/patterns.md "Calling Another
18
+ # narrow interface for what it needs and internal/composition plus
19
+ # cmd/api/wiring.go supply the concrete service — see docs/architect/patterns.md "Calling Another
20
20
  # Domain's Logic". Without this, the first shortcut someone takes stays
21
21
  # invisible until the coupling is everywhere.
22
22
  #
@@ -29,21 +29,23 @@ domain-oriented design, and an optional CQRS style. They are compatible:
29
29
  apply them inside each feature instead of creating a second architecture or
30
30
  an empty layer for every noun.
31
31
 
32
- - **Inbound adapter:** `handler.go` parses HTTP, validates/binds transport
33
- input, calls an application service, and maps the result to HTTP.
34
- - **Application boundary:** `service.go` (or `commands.go`/`queries.go` when
35
- the module uses `--cqrs`; complex auth may also use
36
- `internal/app/<feature>/application/`) orchestrates use cases. It owns
32
+ - **Inbound adapter:** `adapters/inbound/http/dto.go` owns JSON/binding request
33
+ DTOs, response DTOs, and mapping at the HTTP boundary; `handler.go` parses
34
+ HTTP and calls the relevant application service or command/query port.
35
+ - **Application boundary:** `application/service.go` (or
36
+ `application/commands.go`/`application/queries.go` when
37
+ the module uses `--cqrs`) orchestrates use cases. It owns
37
38
  workflow decisions and depends on narrow consumer-owned ports.
38
- - **Domain:** models, value rules, and invariants stay independent of Gin,
39
+ - **Domain:** entities, value rules, and invariants stay independent of Gin,
39
40
  GORM, Redis, HTTP status codes, and environment variables.
40
- - **Outbound adapter:** `repository.go`, token stores, mail/queue clients,
41
+ - **Outbound adapter:** `adapters/outbound/postgres/repository.go`, token
42
+ stores, mail/queue clients,
41
43
  and other platform integrations translate a port into persistence or I/O.
42
44
  - **Composition:** the feature-local `composition.go` constructs its
43
- repository, service, and handler. `cmd/api/wiring.go` selects shared
44
- infrastructure, supplies explicitly declared cross-feature ports/security
45
- dependencies, and registers the feature route; it does not contain
46
- business logic or feature-internal constructors.
45
+ repository, application boundary, and handler. `internal/composition/` owns
46
+ process-level cross-feature adapters. `cmd/api/wiring.go` selects shared
47
+ infrastructure, supplies those adapters and security dependencies, and
48
+ registers feature routes; it does not contain cross-feature adapter logic.
47
49
 
48
50
  The project defaults new modules to `{{defaultApplicationStyle}}` application
49
51
  handling and a `{{defaultModuleSurface}}` surface. The `generate module` wizard
@@ -61,7 +63,9 @@ folder only to satisfy a label.
61
63
  Keep feature boundaries real. Do not import another feature's private model,
62
64
  repository, or handler. Expose a small public application port, call a
63
65
  documented feature API, or use an explicit event when cross-feature behavior
64
- is required. Keep dependency direction flowing inward:
66
+ is required. Put synchronous adapters in `internal/composition/`, import only
67
+ the other feature's public package root, and keep one-way dependencies. Keep
68
+ dependency direction flowing inward:
65
69
 
66
70
  ```text
67
71
  handler -> application/use case -> domain + ports -> adapters
@@ -70,15 +74,28 @@ handler -> application/use case -> domain + ports -> adapters
70
74
  ```
71
75
 
72
76
  The generated CRUD shape is an intentionally incremental starter. It gives
73
- each module a testable service/handler seam and a feature-local composition
74
- root; it does not decide the final aggregate, fields, invariants, or split
75
- between commands and queries for the product.
77
+ each module a testable application/inbound seam and a feature-local
78
+ composition root; it does not decide the final aggregate, fields, invariants,
79
+ or split between commands and queries for the product.
80
+
81
+ For a CQRS module, `application/commands.go` and
82
+ `application/queries.go` are the primary application boundary and
83
+ `composition.go` passes separate command and query handlers to the inbound
84
+ adapter's `NewHandler`. A CQRS module must not also grow an
85
+ `application/service.go` facade. Service and CQRS may coexist across modules;
86
+ they are mutually exclusive inside one module.
87
+
88
+ Auth and RBAC are security-sensitive modules, but they use the same canonical
89
+ split boundary as every other feature. Auth owns user application use cases
90
+ and ports; RBAC owns role policy and authorization adapters. Their public
91
+ module roots compose those pieces, and one canonical implementation tree is
92
+ generated.
76
93
 
77
94
  ## Hard rule: use the CLI for generated surface
78
95
 
79
96
  If you are creating a **new domain** (`internal/app/<name>/` with its own
80
- model/service/repository/handler), or adding a **new endpoint** to an existing
81
- domain, stop and run the CLI:
97
+ domain/application/ports/adapters boundary), or adding a **new endpoint** to
98
+ an existing domain, stop and run the CLI:
82
99
 
83
100
  ```bash
84
101
  go-scaffold generate module <name> --defaults
@@ -107,8 +124,8 @@ Recognize indirect requests as generation work:
107
124
 
108
125
  Run `go-scaffold generate module --help` or `generate method --help` when the
109
126
  desired shape is unclear. Do not hand-edit generated route markers to add a
110
- new endpoint; the CLI keeps handler, service interface, test stub, and
111
- OpenAPI/migration patching in sync.
127
+ new endpoint; the CLI keeps the inbound adapter, application port, test seam,
128
+ and OpenAPI/migration patching in sync.
112
129
 
113
130
  ## What the CLI does not decide
114
131
 
@@ -166,13 +183,11 @@ approval before changing their contract.
166
183
  - Password reset and email verification consume a one-time token in the same
167
184
  retry-safe transaction as the user/identity update. A failed post-commit
168
185
  session revocation must not make a successful reset impossible to retry.
169
- - The generated auth facade is a pragmatic compatibility boundary: its service
170
- contracts currently use the feature's `model` values and shared `apperror`,
171
- while handlers own Gin and provider adapters own SDK configuration. Do not
172
- broaden that coupling by importing Gin, provider SDKs, or HTTP handlers into
173
- use cases. New complex flows should prefer transport-neutral DTOs and narrow
174
- ports; a deeper separation of the existing auth facade is an explicit
175
- architecture refactor and needs focused tests.
186
+ - The generated auth implementation is canonical: use cases live in
187
+ `application/`, domain rules in `domain/`, contracts in `ports/`, and
188
+ provider/HTTP/Postgres details in their adapters. Do not import Gin, provider
189
+ SDKs, or persistence models into new application use cases; keep the
190
+ token/OAuth/MFA tests as the security gate.
176
191
  - Browser provider exchange consumes a one-time server-side transaction bound
177
192
  to provider, state, S256 PKCE challenge, and OIDC nonce. Google ID tokens
178
193
  must be signature, issuer, audience/azp, time-claim, nonce, and subject
@@ -231,6 +246,10 @@ correctness. MFA state still requires PostgreSQL even when refresh tokens use
231
246
  Redis. When routes or OpenAPI templates change, inspect the updated
232
247
  `docs/openapi.yaml` and run the configured OpenAPI linter.
233
248
 
249
+ When a River worker is installed, run `make river-migrate-test` in addition to
250
+ `make migrate-up-test`. The queue integration test must enqueue a real job and
251
+ observe the worker handler; a worker process that merely boots is not enough.
252
+
234
253
  Only in the `go-scaffold` source repository, run the generator project's
235
254
  `pnpm run verify` for CLI/template changes, then generate fresh sample projects
236
255
  to inspect the output. A generated Go project does not contain the generator's
@@ -244,19 +263,24 @@ work untouched.
244
263
 
245
264
  ## Command quick reference
246
265
 
247
- - `go-scaffold generate module <name> --defaults` — minimal model, errors,
248
- repository, service/handler seams, feature-local `composition.go`, root
249
- route registration, and versioned module migrations. Add endpoints with
266
+ - `go-scaffold generate module <name> --defaults` — minimal domain entity,
267
+ errors, ports, application boundary, inbound/outbound adapters,
268
+ feature-local `composition.go`, root route registration, and versioned module
269
+ migrations. Add endpoints with
250
270
  `generate method`, use `--profile crud` for a CRUD skeleton, or
251
271
  `--profile cqrs` for separate command/query application handlers.
252
272
  - `go-scaffold generate method <module> <name> --type <get|post|put|patch|delete> [--get-mode all|one] [--field <name>]` — patches an existing
253
- module's handler/service and, for a GET-one lookup, repository plus test
254
- seam. It never overwrites a same-named method.
273
+ module's inbound handler, application service or command/query handler and,
274
+ for a GET-one lookup, its repository port/adapter plus test seam. It never
275
+ overwrites a same-named method.
255
276
  - `go-scaffold generate migration <name>` — reserves a timestamped SQL
256
277
  migration pair for a reviewed schema change.
257
278
  - `go-scaffold config` — interactively change future module defaults;
258
279
  `config show` prints the resolved manifest and `config validate` checks it
259
280
  without writing.
281
+ - `go-scaffold check` — validates every module's split hexagonal layout,
282
+ forbidden layer dependencies, sibling imports, and the exclusive
283
+ service-versus-CQRS contract.
260
284
  - `go-scaffold undo module <name> -y` — removes an unshipped generated module,
261
285
  its migration, and its wiring. It refuses once those migrations are
262
286
  committed or applied; use a reviewed drop migration for a shipped domain.
@@ -22,28 +22,55 @@ for scripts.
22
22
 
23
23
  See `docs/architect/` for the conventions every generated module follows, and `AGENTS.md`/`CLAUDE.md` if you're working with an AI coding agent in this repo.
24
24
 
25
+ ## Installed optional features
26
+
27
+ This project records its generated capabilities in `go-scaffold.config.json`:
28
+
29
+ - authentication: {{#if auth}}enabled{{else}}not installed — use `go-scaffold add auth` when needed{{/if}}
30
+ - background jobs: {{#if worker}}enabled{{else}}not installed — use `go-scaffold add worker` when needed{{/if}}
31
+ - RBAC: {{#if rbac}}enabled{{else}}not installed — use `go-scaffold add rbac` after auth when needed{{/if}}
32
+ - metrics and tracing: {{#if observability}}enabled{{else}}not installed — use `go-scaffold add observability` when needed{{/if}}
33
+
34
+ The `add` commands refresh untouched generated architecture docs and this
35
+ section. If a maintainer has edited a document, the command leaves it alone
36
+ and reports that it needs a manual update.
37
+
25
38
  ## Layout
26
39
 
27
40
  ```
28
41
  cmd/
29
- └── api/
30
- ├── main.go # process entry point, config, slog, graceful shutdown
31
- └── wiring.go # composition root: infrastructure + domain registration
42
+ ├── api/
43
+ ├── main.go # process entry point, config, slog, graceful shutdown
44
+ └── wiring.go # process composition root and route registration
32
45
  internal/
33
- ├── platform/ # talks to real external systems (DB, later: cache, queue, mail, ...)
34
- │ └── database/ # opens the connection + pool (GORM)
46
+ ├── composition/ # cross-feature adapters and process-level wiring
47
+ ├── platform/ # adapters for external systems
48
+ │ └── database/ # opens the PostgreSQL connection + pool
35
49
  ├── shared/ # pure logic/framework glue, no I/O
36
50
  │ ├── config/ # loads config from env
37
- │ ├── apperror/ # central error type (status + payload)
38
- │ ├── dberr/ # maps DB errors to constraint kind (IsDuplicate, IsForeignKey) — shared by every domain
39
- │ ├── httpx/ # HTTP helpers shared by every domain (ParseID, BindErr)
40
- │ ├── id/ # UUID v7 generation (id.New) — app-side, not a DB default
41
- │ ├── middleware/ # RequestID, Logger (slog), Error, CORS
42
- │ ├── pagination/ # parses ?limit=&offset=, response envelope
43
- │ └── tx/ # carries a transaction on the ctx (tx.Do / tx.From) so two repositories commit together
44
- └── app/ # domain packages empty until you `generate module`
51
+ │ ├── apperror/ # central error type (status + payload)
52
+ │ ├── dberr/ # maps DB errors to constraint kind — shared by every domain
53
+ │ ├── httpx/ # HTTP helpers shared by every domain
54
+ │ ├── id/ # UUID v7 generation (id.New)
55
+ │ ├── middleware/ # RequestID, Logger, Error, CORS{{#if auth}}, auth{{/if}}{{#if rbac}}, RBAC{{/if}}{{#if observability}}, metrics, tracing{{/if}}
56
+ │ ├── pagination/ # parses ?limit=&offset=, response envelope
57
+ │ └── tx/ # carries a transaction on the context
58
+ └── app/ # one package per domain, added with `generate module`
45
59
  ```
46
60
 
61
+ The tree shows the baseline. Installed optional features add the process and
62
+ platform packages listed in the section above without changing these module
63
+ boundaries.
64
+
65
+ Cross-feature adapters belong in `internal/composition/`, not in `cmd/api/`.
66
+ The API wiring file selects infrastructure, invokes those adapters, and
67
+ registers routes. Keep one-way dependencies and expose only narrow public
68
+ capabilities from each feature.
69
+
70
+ Optional features stay in the same architecture: auth owns user application
71
+ use cases and provider ports, RBAC owns role policy, worker owns queue/mail
72
+ adapters, and observability owns metrics/tracing setup.
73
+
47
74
  ## Run
48
75
 
49
76
  ```bash
@@ -52,6 +79,9 @@ make docker-up # local Postgres (postgres:5432)
52
79
  {{/if}}
53
80
  make db-create # create the {{dbName}} database itself (once — safe to re-run)
54
81
  go mod tidy
82
+ {{#if worker}}{{#if (eq queue "river")}}make river-migrate # create River's job tables before queued mail
83
+ {{else}}# start Redis and set REDIS_URL before running the worker
84
+ {{/if}}{{/if}}
55
85
  make run # APP_ENV=development enables the convenience schema bootstrap
56
86
  ```
57
87
 
@@ -75,17 +105,27 @@ falls back to `docker exec` into whichever container is publishing
75
105
  other Postgres container{{else}}any Postgres container{{/if}} you already have running.
76
106
 
77
107
  Server listens on `:8080` (override with `PORT`). Ctrl+C = graceful shutdown.
108
+ {{#if worker}}`make dev` starts both the HTTP API and the worker; `make run` starts only the API.
109
+ {{/if}}
78
110
 
79
111
  ## Makefile
80
112
 
81
113
  ```bash
82
114
  make run # go run ./cmd/api
83
- make build # go build -o bin/api ./cmd/api
115
+ {{#if worker}}make dev # run cmd/api and cmd/worker together
116
+ make worker # run cmd/worker separately
117
+ {{#if (eq queue "river")}}make river-migrate # create/update River's Postgres job tables
118
+ make river-migrate-test # same River tables in TEST_DB_DSN
119
+ {{else}}# start Redis before running make worker
120
+ {{/if}}
121
+ {{/if}}{{#if auth}}make seed # run one-shot cmd/seed
122
+ {{/if}}make build # go build -o bin/api ./cmd/api
84
123
  make test # go test ./...
85
124
  make fmt # gofmt -w .
86
125
  make vet # go vet ./...
87
126
  make lint # golangci-lint run (see .golangci.yml)
88
127
  make tidy # go mod tidy
128
+ make tools # install pinned migrate and golangci-lint tools
89
129
  make db-create # create the database itself (safe to re-run)
90
130
  make db-drop # drop the database
91
131
  make migrate-up # apply migrations (reads DB_DSN from ENV_FILE)
@@ -176,9 +216,17 @@ make docker-up # local postgres first
176
216
  {{/if}}
177
217
  make db-create DB_NAME={{dbName}}_test
178
218
  make migrate-up-test
219
+ {{#if (eq queue "river")}}make river-migrate-test
220
+ {{/if}}
179
221
  TEST_DB_DSN=postgres://postgres:postgres@localhost:5432/{{dbName}}_test?sslmode=disable REQUIRE_TEST_DB=true go test ./...
180
222
  ```
181
223
 
224
+ {{#if (eq queue "river")}}The River integration test also starts a real worker,
225
+ enqueues a JSON job, and waits for its backend-neutral handler. River's own
226
+ schema history is separate from this project's application migrations, and the
227
+ `river-migrate` targets use the pinned River version from `go.mod`.
228
+ {{/if}}
229
+
182
230
  ## Error payload
183
231
 
184
232
  ```json
@@ -208,32 +256,39 @@ go-scaffold generate module orders
208
256
  ```
209
257
 
210
258
  Scaffolds an `internal/app/order/` module using the project defaults
211
- (`{{defaultModuleSurface}}` + `{{defaultApplicationStyle}}`), wires its route/
212
- model into `cmd/api/wiring.go`, and appends a migration file. Add endpoints
259
+ (`{{defaultModuleSurface}}` + `{{defaultApplicationStyle}}`), wires its route
260
+ into `cmd/api/wiring.go`, and appends a migration file. Add endpoints
213
261
  with `generate method`; use `--profile crud` for a CRUD skeleton or
214
262
  `--profile cqrs` for separate command/query handlers. See
215
263
  `docs/architect/patterns.md` for the module shape and foreign-key rules.
216
264
 
217
265
  ## Deploying
218
266
 
267
+ {{#if worker}}
219
268
  Two processes, one image, one target each:
220
269
 
221
270
  ```bash
222
271
  docker build --target api -t {{projectName}}-api .
223
- docker build --target worker -t {{projectName}}-worker . # once `add worker` exists
272
+ docker build --target worker -t {{projectName}}-worker .
224
273
  ```
225
274
 
226
275
  `api` is the last stage, so a plain `docker build .` builds it — the worker
227
- stage has to be asked for by name, and only resolves once `add worker` has
228
- created `cmd/worker`.
276
+ stage has to be asked for by name.{{#if auth}} `cmd/seed` is a one-shot
277
+ operational binary run through `make seed`, not a long-running target.{{/if}}
278
+ {{else}}
279
+ The base project has one long-running `cmd/api` image. A worker target becomes
280
+ available after `go-scaffold add worker`; do not deploy a worker that has not
281
+ been generated.
282
+ {{/if}}
229
283
 
230
284
  Both stages are distroless and run as `nonroot`, so there is no shell in either
231
285
  image. Debug against the `build` stage instead:
232
286
  `docker run --rm -it --entrypoint sh $(docker build -q --target build .)`.
233
287
 
234
- `cmd/api` serves HTTP. `cmd/worker` consumes the queue and is a separate
235
- deployment with **no port and no health endpoint** — scale it independently,
236
- and remember that not running it means queued mail is never sent, silently.
288
+ `cmd/api` serves HTTP.{{#if worker}} `cmd/worker` consumes the queue and is a
289
+ separate deployment with **no port and no health endpoint** — scale it
290
+ independently, and remember that not running it means queued mail is never
291
+ sent, silently.{{/if}}
237
292
 
238
293
  Before the first deploy of a release:
239
294
 
@@ -22,7 +22,8 @@ import (
22
22
  // run is this binary's composition root — the only function that sees every
23
23
  // module and wires them to each other. Modules never import one another; a
24
24
  // domain that needs another's behaviour declares a narrow interface and gets
25
- // the concrete service from here (see docs/architect/patterns.md).
25
+ // its concrete implementation from the process composition package (see
26
+ // docs/architect/patterns.md).
26
27
  //
27
28
  // It returns an error rather than calling os.Exit, so every defer below runs
28
29
  // on the way out and main() holds the only exit in the binary.
@@ -55,7 +56,7 @@ func run() error {
55
56
 
56
57
  if !cfg.IsProd() {
57
58
  // Development boot creates tables but never the schema they live in — each
58
- // domain gets its own (see model.go's TableName), so it has to exist
59
+ // domain gets its own (see the outbound postgres model's TableName), so it has to exist
59
60
  // before the bootstrap runs. Production runs the versioned SQL migrations
60
61
  // instead, so this entire branch is skipped there.
61
62
  // go-scaffold:schemas