@nakedev/go-scaffold 0.4.0 → 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 (192) hide show
  1. package/README.md +598 -306
  2. package/dist/commands/auth.js +65 -23
  3. package/dist/commands/check.js +281 -0
  4. package/dist/commands/config.js +50 -0
  5. package/dist/commands/create.js +33 -2
  6. package/dist/commands/generate.js +29 -3
  7. package/dist/commands/method.js +74 -63
  8. package/dist/commands/migration.js +2 -2
  9. package/dist/commands/observability.js +4 -53
  10. package/dist/commands/rbac.js +21 -10
  11. package/dist/commands/undo.js +11 -3
  12. package/dist/commands/worker.js +15 -5
  13. package/dist/index.js +198 -59
  14. package/dist/prompts/auth-wizard.js +40 -6
  15. package/dist/prompts/create-wizard.js +42 -1
  16. package/dist/prompts/generate-wizard.js +89 -9
  17. package/dist/templates/auth-manifest.js +50 -19
  18. package/dist/templates/create-manifest.js +8 -0
  19. package/dist/templates/module-manifest.js +84 -26
  20. package/dist/templates/rbac-manifest.js +16 -11
  21. package/dist/templates/worker-manifest.js +4 -1
  22. package/dist/types.js +8 -0
  23. package/dist/utils/auth-patcher.js +124 -33
  24. package/dist/utils/config.js +167 -4
  25. package/dist/utils/docs-patcher.js +68 -0
  26. package/dist/utils/hexagonal-method-patcher.js +334 -0
  27. package/dist/utils/main-patcher.js +32 -30
  28. package/dist/utils/marker-patch.js +7 -1
  29. package/dist/utils/module-location.js +17 -11
  30. package/dist/utils/module-profile.js +32 -0
  31. package/dist/utils/platform-patcher.js +56 -7
  32. package/dist/utils/rbac-patcher.js +89 -210
  33. package/package.json +7 -2
  34. package/templates/add/auth/cmd/seed/main.go.hbs +15 -3
  35. package/templates/add/auth/docs/login.yaml.hbs +11 -1
  36. package/templates/add/auth/docs/mfa-verify.yaml.hbs +19 -0
  37. package/templates/add/auth/docs/provider-exchange.yaml.hbs +40 -0
  38. package/templates/add/auth/docs/provider-login.yaml.hbs +31 -0
  39. package/templates/add/auth/docs/refresh.yaml.hbs +7 -0
  40. package/templates/add/auth/docs/register.yaml.hbs +7 -0
  41. package/templates/add/auth/docs/reset-password.yaml.hbs +1 -1
  42. package/templates/add/auth/docs/schemas.yaml.hbs +59 -1
  43. package/templates/add/auth/docs/users-me-mfa-confirm.yaml.hbs +19 -0
  44. package/templates/add/auth/docs/users-me-mfa-disable.yaml.hbs +15 -0
  45. package/templates/add/auth/docs/users-me-mfa-setup.yaml.hbs +14 -0
  46. package/templates/add/auth/docs/users-me-mfa.yaml.hbs +12 -0
  47. package/templates/add/auth/internal/app/user/adapters/inbound/http/browser_policy.go.hbs +98 -0
  48. package/templates/add/auth/internal/app/user/adapters/inbound/http/dto.go.hbs +159 -0
  49. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler.go.hbs +228 -0
  50. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_local.go.hbs +76 -0
  51. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_mfa.go.hbs +83 -0
  52. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_oauth.go.hbs +70 -0
  53. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_recovery.go.hbs +49 -0
  54. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_test.go.hbs +311 -0
  55. package/templates/add/auth/internal/app/user/adapters/inbound/http/handler_user.go.hbs +41 -0
  56. package/templates/add/auth/internal/app/user/adapters/inbound/http/session_cookie.go.hbs +35 -0
  57. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt.go.hbs +35 -0
  58. package/templates/add/auth/internal/app/user/adapters/outbound/password/bcrypt_test.go.hbs +20 -0
  59. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store.go.hbs +129 -0
  60. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/mfa_store_test.go.hbs +174 -0
  61. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/model.go.hbs +84 -0
  62. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/repository.go.hbs +211 -0
  63. package/templates/add/auth/internal/app/user/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +18 -19
  64. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg.go.hbs +213 -0
  65. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_pg_test.go.hbs +103 -0
  66. package/templates/add/auth/internal/app/user/adapters/outbound/postgres/tokenstore_recovery.go.hbs +84 -0
  67. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore.go.hbs +228 -0
  68. package/templates/add/auth/internal/app/user/adapters/outbound/redis/tokenstore_test.go.hbs +196 -0
  69. package/templates/add/auth/internal/app/user/application/contracts.go.hbs +52 -0
  70. package/templates/add/auth/internal/app/user/application/dto.go.hbs +75 -0
  71. package/templates/add/auth/internal/app/user/application/errors.go.hbs +62 -0
  72. package/templates/add/auth/internal/app/user/application/external_login.go.hbs +198 -0
  73. package/templates/add/auth/internal/app/user/application/jwt.go.hbs +58 -0
  74. package/templates/add/auth/internal/app/user/application/local_auth.go.hbs +96 -0
  75. package/templates/add/auth/internal/app/user/application/mfa_service.go.hbs +449 -0
  76. package/templates/add/auth/internal/app/user/application/mfa_service_test.go.hbs +200 -0
  77. package/templates/add/auth/internal/app/user/application/oauth.go.hbs +132 -0
  78. package/templates/add/auth/internal/app/user/application/provider_test.go.hbs +285 -0
  79. package/templates/add/auth/internal/app/user/application/recovery.go.hbs +82 -0
  80. package/templates/add/auth/internal/app/user/application/recovery_service.go.hbs +112 -0
  81. package/templates/add/auth/internal/app/user/application/service.go.hbs +145 -0
  82. package/templates/add/auth/internal/app/user/application/service_test.go.hbs +891 -0
  83. package/templates/add/auth/internal/app/user/application/sessions.go.hbs +99 -0
  84. package/templates/add/auth/internal/app/user/application/tokenstore_ports.go.hbs +14 -0
  85. package/templates/add/auth/internal/app/user/application/user_query.go.hbs +65 -0
  86. package/templates/add/auth/internal/app/user/composition.go.hbs +168 -0
  87. package/templates/add/auth/internal/app/user/domain/entity.go.hbs +41 -0
  88. package/templates/add/auth/internal/app/user/domain/errors.go.hbs +32 -0
  89. package/templates/add/auth/internal/app/user/ports/password.go.hbs +9 -0
  90. package/templates/add/auth/internal/app/user/ports/repository.go.hbs +90 -0
  91. package/templates/add/auth/internal/platform/authprovider/google/google.go.hbs +389 -0
  92. package/templates/add/auth/internal/platform/authprovider/google/google_test.go.hbs +312 -0
  93. package/templates/add/auth/migrations/create_auth_tokens.up.sql.hbs +10 -5
  94. package/templates/add/auth/migrations/create_identities.up.sql.hbs +1 -1
  95. package/templates/add/auth/migrations/create_login_throttle.up.sql.hbs +1 -1
  96. package/templates/add/auth/migrations/create_mfa.down.sql.hbs +3 -0
  97. package/templates/add/auth/migrations/create_mfa.up.sql.hbs +29 -0
  98. package/templates/add/auth/migrations/create_users.up.sql.hbs +4 -3
  99. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler.go.hbs +142 -0
  100. package/templates/add/rbac/internal/app/role/adapters/inbound/http/handler_test.go.hbs +19 -0
  101. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/model.go.hbs +48 -0
  102. package/templates/add/rbac/internal/app/role/adapters/outbound/postgres/repository.go.hbs +127 -0
  103. package/templates/add/rbac/internal/app/role/{repository_test.go.hbs → adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  104. package/templates/add/rbac/internal/app/role/application/dto.go.hbs +47 -0
  105. package/templates/add/rbac/internal/app/role/application/errors.go.hbs +19 -0
  106. package/templates/add/rbac/internal/app/role/application/service.go.hbs +157 -0
  107. package/templates/add/rbac/internal/app/role/{service_test.go.hbs → application/service_test.go.hbs} +26 -19
  108. package/templates/add/rbac/internal/app/role/composition.go.hbs +48 -0
  109. package/templates/add/rbac/internal/app/role/domain/entity.go.hbs +23 -0
  110. package/templates/add/rbac/internal/app/role/domain/errors.go.hbs +26 -0
  111. package/templates/add/rbac/internal/app/role/ports/repository.go.hbs +25 -0
  112. package/templates/add/rbac/migrations/add_roles.down.sql.hbs +3 -11
  113. package/templates/add/rbac/migrations/add_roles.up.sql.hbs +17 -6
  114. package/templates/add/worker/internal/platform/queue/river_test.go.hbs +84 -0
  115. package/templates/create/base/.claude/skills/go-scaffold/SKILL.md.hbs +358 -121
  116. package/templates/create/base/.env.example.hbs +0 -1
  117. package/templates/create/base/.golangci.yml.hbs +2 -2
  118. package/templates/create/base/AGENTS.md.hbs +279 -67
  119. package/templates/create/base/Makefile.hbs +2 -1
  120. package/templates/create/base/README.md.hbs +115 -32
  121. package/templates/create/base/cmd/api/wiring.go.hbs +13 -9
  122. package/templates/create/base/internal/composition/doc.go.hbs +7 -0
  123. package/templates/create/base/internal/platform/database/database.go.hbs +3 -3
  124. package/templates/create/base/internal/shared/apperror/apperror.go.hbs +15 -2
  125. package/templates/create/base/internal/shared/config/config.go.hbs +0 -8
  126. package/templates/create/base/internal/shared/middleware/cors_test.go.hbs +40 -0
  127. package/templates/create/base/internal/shared/middleware/error.go.hbs +15 -5
  128. package/templates/create/features/docs/architecture.md.hbs +92 -32
  129. package/templates/create/features/docs/patterns.md.hbs +137 -91
  130. package/templates/create/features/docs/techstack.md.hbs +18 -3
  131. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.go.hbs +45 -0
  132. package/templates/generate/module/hexagonal/adapters/inbound/http/dto.minimal.go.hbs +28 -0
  133. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.go.hbs +182 -0
  134. package/templates/generate/module/hexagonal/adapters/inbound/http/handler.minimal.go.hbs +83 -0
  135. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_crud_test.go.hbs +18 -0
  136. package/templates/generate/module/hexagonal/adapters/inbound/http/handler_test.go.hbs +30 -0
  137. package/templates/generate/module/hexagonal/adapters/outbound/postgres/model.go.hbs +37 -0
  138. package/templates/generate/module/hexagonal/adapters/outbound/postgres/repository.go.hbs +95 -0
  139. package/templates/generate/module/{repository_test.go.hbs → hexagonal/adapters/outbound/postgres/repository_test.go.hbs} +8 -8
  140. package/templates/generate/module/hexagonal/application/commands.crud.go.hbs +54 -0
  141. package/templates/generate/module/hexagonal/application/commands.go.hbs +25 -0
  142. package/templates/generate/module/hexagonal/application/cqrs_test.go.hbs +66 -0
  143. package/templates/generate/module/hexagonal/application/dto.go.hbs +35 -0
  144. package/templates/generate/module/hexagonal/application/dto.minimal.go.hbs +25 -0
  145. package/templates/generate/module/hexagonal/application/queries.crud.go.hbs +33 -0
  146. package/templates/generate/module/hexagonal/application/queries.go.hbs +25 -0
  147. package/templates/generate/module/hexagonal/application/service.crud.go.hbs +73 -0
  148. package/templates/generate/module/hexagonal/application/service.go.hbs +29 -0
  149. package/templates/generate/module/hexagonal/application/service_test.go.hbs +62 -0
  150. package/templates/generate/module/hexagonal/composition.go.hbs +27 -0
  151. package/templates/generate/module/hexagonal/domain/entity.go.hbs +20 -0
  152. package/templates/generate/module/hexagonal/domain/errors.go.hbs +11 -0
  153. package/templates/generate/module/hexagonal/ports/repository.go.hbs +38 -0
  154. package/templates/generate/module/migration.up.sql.hbs +1 -1
  155. package/dist/utils/method-patcher.js +0 -357
  156. package/templates/add/auth/docs/google-callback.yaml.hbs +0 -22
  157. package/templates/add/auth/docs/google-login.yaml.hbs +0 -7
  158. package/templates/add/auth/internal/app/user/dto.go.hbs +0 -77
  159. package/templates/add/auth/internal/app/user/errors.go.hbs +0 -43
  160. package/templates/add/auth/internal/app/user/handler.go.hbs +0 -276
  161. package/templates/add/auth/internal/app/user/jwt.go.hbs +0 -108
  162. package/templates/add/auth/internal/app/user/model/authtoken.go.hbs +0 -39
  163. package/templates/add/auth/internal/app/user/model/identity.go.hbs +0 -31
  164. package/templates/add/auth/internal/app/user/model/loginthrottle.go.hbs +0 -26
  165. package/templates/add/auth/internal/app/user/model/user.go.hbs +0 -30
  166. package/templates/add/auth/internal/app/user/repository.go.hbs +0 -137
  167. package/templates/add/auth/internal/app/user/service.go.hbs +0 -531
  168. package/templates/add/auth/internal/app/user/service_test.go.hbs +0 -316
  169. package/templates/add/auth/internal/app/user/tokenstore.go.hbs +0 -30
  170. package/templates/add/auth/internal/app/user/tokenstore_pg.go.hbs +0 -144
  171. package/templates/add/auth/internal/app/user/tokenstore_redis.go.hbs +0 -147
  172. package/templates/add/rbac/internal/app/role/dto.go.hbs +0 -45
  173. package/templates/add/rbac/internal/app/role/errors.go.hbs +0 -39
  174. package/templates/add/rbac/internal/app/role/handler.go.hbs +0 -104
  175. package/templates/add/rbac/internal/app/role/model/permission.go.hbs +0 -12
  176. package/templates/add/rbac/internal/app/role/model/role.go.hbs +0 -22
  177. package/templates/add/rbac/internal/app/role/model/role_permission.go.hbs +0 -11
  178. package/templates/add/rbac/internal/app/role/repository.go.hbs +0 -97
  179. package/templates/add/rbac/internal/app/role/service.go.hbs +0 -217
  180. package/templates/generate/module/dto.go.hbs +0 -36
  181. package/templates/generate/module/errors.go.hbs +0 -33
  182. package/templates/generate/module/handler.go.hbs +0 -134
  183. package/templates/generate/module/handler_test.go.hbs +0 -174
  184. package/templates/generate/module/minimal/dto.go.hbs +0 -28
  185. package/templates/generate/module/minimal/handler.go.hbs +0 -48
  186. package/templates/generate/module/minimal/handler_test.go.hbs +0 -10
  187. package/templates/generate/module/minimal/service.go.hbs +0 -45
  188. package/templates/generate/module/minimal/service_test.go.hbs +0 -77
  189. package/templates/generate/module/model/model.go.hbs +0 -36
  190. package/templates/generate/module/repository.go.hbs +0 -103
  191. package/templates/generate/module/service.go.hbs +0 -108
  192. package/templates/generate/module/service_test.go.hbs +0 -161
@@ -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.
@@ -46,17 +47,20 @@ func run() error {
46
47
  if err != nil {
47
48
  return fmt.Errorf("db handle: %w", err)
48
49
  }
50
+ defer func() {
51
+ if err := sqlDB.Close(); err != nil {
52
+ logger.Error("close db", "error", err)
53
+ }
54
+ }()
49
55
  // go-scaffold:platform-init
50
56
 
51
- // AutoMigrate creates tables but never the schema they live in — each
52
- // domain gets its own (see model.go's TableName), so it has to exist
53
- // before AutoMigrate runs. The versioned SQL migrations create the same
54
- // schemas with CREATE SCHEMA IF NOT EXISTS, so this only matters for dev.
55
- // go-scaffold:schemas
57
+ if !cfg.IsProd() {
58
+ // Development boot creates tables but never the schema they live in — each
59
+ // domain gets its own (see the outbound postgres model's TableName), so it has to exist
60
+ // before the bootstrap runs. Production runs the versioned SQL migrations
61
+ // instead, so this entire branch is skipped there.
62
+ // go-scaffold:schemas
56
63
 
57
- if cfg.AutoMigrate {
58
- // ponytail: AutoMigrate is for dev only (add-only, locks the table once data grows)
59
- // prod: set AUTO_MIGRATE=false and run golang-migrate as versioned SQL instead
60
64
  if err := db.AutoMigrate(
61
65
  // go-scaffold:models
62
66
  ); err != nil {
@@ -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
@@ -20,7 +20,8 @@ import (
20
20
 
21
21
  // Open connects to Postgres and sets the connection pool — it talks to a
22
22
  // real external system, so it lives in platform/, not shared/.
23
- // Schema is managed via golang-migrate (migrations/) in prod, not AutoMigrate — see README.
23
+ // Production schema is managed via golang-migrate (migrations/); development
24
+ // may use the convenience path selected by APP_ENV in the composition root.
24
25
  func Open(cfg config.Config) (*gorm.DB, error) {
25
26
  // GORM's default logger reports ErrRecordNotFound at ERROR level, so an
26
27
  // ordinary miss — every FindByID behind a 404, cmd/seed's "does this admin
@@ -58,8 +59,7 @@ var migrationVersionRe = regexp.MustCompile(`^(\d+)_.*\.up\.sql$`)
58
59
  // golang-migrate CLI's own schema_migrations table) doesn't match the newest
59
60
  // migration file baked into this binary — instead of booting against a stale
60
61
  // or half-applied schema and failing later on whatever query hits the
61
- // missing column first. Only meaningful when AutoMigrate is off (prod); call
62
- // this from that branch only.
62
+ // missing column first. Call this from the production branch only.
63
63
  func CheckMigrationVersion(db *gorm.DB) error {
64
64
  entries, err := migrations.FS.ReadDir(".")
65
65
  if err != nil {
@@ -10,10 +10,15 @@ type AppError struct {
10
10
  Message string `json:"message"`
11
11
  Details any `json:"details,omitempty"`
12
12
  RequestID string `json:"request_id,omitempty"` // filled in by the error middleware
13
+ cause error
13
14
  }
14
15
 
15
16
  func (e *AppError) Error() string { return e.Message }
16
17
 
18
+ // Unwrap keeps the technical cause available to structured logging and
19
+ // errors.Is/As without putting it in the JSON response.
20
+ func (e *AppError) Unwrap() error { return e.cause }
21
+
17
22
  func New(status int, code, msg string) *AppError {
18
23
  return &AppError{HTTPStatus: status, Code: code, Message: msg}
19
24
  }
@@ -30,7 +35,15 @@ func NewConflict(msg string) *AppError {
30
35
  return &AppError{HTTPStatus: http.StatusConflict, Code: "CONFLICT", Message: msg}
31
36
  }
32
37
 
33
- func NewInternal() *AppError {
38
+ func NewInternal(cause ...error) *AppError {
34
39
  // ponytail: never leak internal details to the client, log separately in middleware
35
- return &AppError{HTTPStatus: http.StatusInternalServerError, Code: "INTERNAL", Message: "internal server error"}
40
+ err := &AppError{HTTPStatus: http.StatusInternalServerError, Code: "INTERNAL", Message: "internal server error"}
41
+ if len(cause) > 0 {
42
+ err.cause = cause[0]
43
+ }
44
+ return err
45
+ }
46
+
47
+ func NewNotImplemented() *AppError {
48
+ return &AppError{HTTPStatus: http.StatusNotImplemented, Code: "NOT_IMPLEMENTED", Message: "not implemented"}
36
49
  }
@@ -14,7 +14,6 @@ type Config struct {
14
14
  Port string
15
15
  DBDSN string
16
16
  LogLevel string
17
- AutoMigrate bool
18
17
  DBMaxOpenConns int
19
18
  DBMaxIdleConns int
20
19
  DBConnMaxLifetime time.Duration
@@ -56,13 +55,6 @@ func Load() Config {
56
55
  panic(fmt.Sprintf("invalid APP_ENV %q — must be development or production", cfg.AppEnv))
57
56
  }
58
57
 
59
- // Defaults off in production, which is why it's set here rather than in
60
- // the literal above — it needs a validated AppEnv first. AutoMigrate
61
- // rewrites the schema at boot AND skips CheckMigrationVersion entirely
62
- // (see main.go), so a deploy that merely forgets to set it used to get
63
- // both. AUTO_MIGRATE=true still forces it on if you really mean it.
64
- cfg.AutoMigrate = env("AUTO_MIGRATE", strconv.FormatBool(!cfg.IsProd())) == "true"
65
-
66
58
  return cfg
67
59
  }
68
60
 
@@ -0,0 +1,40 @@
1
+ package middleware
2
+
3
+ import (
4
+ "net/http"
5
+ "net/http/httptest"
6
+ "testing"
7
+
8
+ "github.com/gin-gonic/gin"
9
+ )
10
+
11
+ func TestCORS_AllowsOnlyConfiguredExactOrigins(t *testing.T) {
12
+ gin.SetMode(gin.TestMode)
13
+ tests := []struct {
14
+ name string
15
+ origin string
16
+ wantAllowOrigin string
17
+ wantCredentials bool
18
+ }{
19
+ {name: "allowed exact origin", origin: "http://localhost:3000", wantAllowOrigin: "http://localhost:3000", wantCredentials: true},
20
+ {name: "different origin denied", origin: "https://evil.example", wantAllowOrigin: "", wantCredentials: false},
21
+ {name: "wildcard is not a substitute", origin: "*", wantAllowOrigin: "", wantCredentials: false},
22
+ }
23
+ for _, tt := range tests {
24
+ t.Run(tt.name, func(t *testing.T) {
25
+ router := gin.New()
26
+ router.Use(CORS([]string{"http://localhost:3000"}))
27
+ router.GET("/refresh", func(c *gin.Context) { c.Status(http.StatusNoContent) })
28
+ request := httptest.NewRequest(http.MethodGet, "/refresh", nil)
29
+ request.Header.Set("Origin", tt.origin)
30
+ response := httptest.NewRecorder()
31
+ router.ServeHTTP(response, request)
32
+ if got := response.Header().Get("Access-Control-Allow-Origin"); got != tt.wantAllowOrigin {
33
+ t.Fatalf("Access-Control-Allow-Origin = %q, want %q", got, tt.wantAllowOrigin)
34
+ }
35
+ if got := response.Header().Get("Access-Control-Allow-Credentials") == "true"; got != tt.wantCredentials {
36
+ t.Fatalf("Allow-Credentials = %v, want %v", got, tt.wantCredentials)
37
+ }
38
+ })
39
+ }
40
+ }
@@ -29,14 +29,24 @@ func Error(exposeDetail bool) gin.HandlerFunc {
29
29
  if !errors.As(err.Err, &appErr) {
30
30
  // Unexpected error: log the real thing, answer the client generically.
31
31
  slog.Error("unhandled error", "error", err.Err, "request_id", c.GetString(RequestIDKey))
32
- appErr = apperror.NewInternal()
32
+ appErr = apperror.NewInternal(err.Err)
33
33
  if exposeDetail {
34
34
  appErr.Details = err.Err.Error()
35
35
  }
36
- } else if !exposeDetail {
37
- // Known AppError (e.g. VALIDATION_ERROR): still strip Details in
38
- // prod, so a direct API call doesn't get field-level hints.
39
- appErr.Details = nil
36
+ } else {
37
+ // Internal AppErrors carry a technical cause for one structured log;
38
+ // known domain errors (validation, not-found, etc.) stay quiet.
39
+ if cause := appErr.Unwrap(); cause != nil {
40
+ slog.Error("request failed", "error", cause, "request_id", c.GetString(RequestIDKey))
41
+ if exposeDetail {
42
+ appErr.Details = cause.Error()
43
+ }
44
+ }
45
+ if !exposeDetail {
46
+ // Known AppError (e.g. VALIDATION_ERROR): still strip Details in
47
+ // prod, so a direct API call doesn't get field-level hints.
48
+ appErr.Details = nil
49
+ }
40
50
  }
41
51
 
42
52
  appErr.RequestID = c.GetString(RequestIDKey)
@@ -2,14 +2,18 @@
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.
8
-
9
- Modules never import one another. A domain that needs another's behaviour
10
- declares a narrow interface for exactly what it needs and receives the concrete
11
- service from `run()`; `.golangci.yml` has a depguard rule per domain that fails
12
- the build if anyone shortcuts that.
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
+
9
+ Modules never import one another. Each generated domain owns its local
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
16
+ depguard rule per domain that fails the build if anyone shortcuts that.
13
17
 
14
18
  Two things the split buys. `run()` returns an error instead of calling
15
19
  `os.Exit`, so the deferred cleanup below it actually runs — `os.Exit` skips
@@ -19,32 +23,49 @@ you open to understand the binary: `wiring.go` grows with the system, which is
19
23
  what a composition root is for, while `main.go` stays put.
20
24
 
21
25
 
22
- > Status: generated by `@nakedev/go-scaffold`
23
- > Scope: initial scaffold
26
+ > Status: generated by `@nakedev/go-scaffold`; refreshes as optional features are added
27
+ > Scope: current generated project configuration
24
28
 
25
29
  ## 1. Topology
26
30
 
27
- **Decision:** Single Go binary (`cmd/api`), REST over Gin.
28
- **Rationale:** One process serves all domains; a second entry point
29
- (`cmd/worker`, `cmd/migrate`) gets its own `main.go` only once there's a real
30
- second process `main.go` has no importers, so adding one later touches no
31
- 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.
32
37
 
33
38
  ## 2. Package Layout
34
39
 
35
40
  ```text
36
41
  cmd/
37
- └── 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
38
45
  internal/
39
- ├── platform/ # talks to real external systems (DB, later: cache, queue, mail, ...)
40
- │ └── database/
46
+ ├── composition/ # process-level cross-feature adapters
47
+ ├── platform/ # talks to real external systems
48
+ │ └── database/ # PostgreSQL connection and pool
41
49
  ├── shared/ # pure logic/framework glue, no I/O
42
50
  │ ├── config/ apperror/ dberr/ httpx/ id/
43
51
  │ └── middleware/ pagination/ tx/
44
52
  └── app/ # domain packages (one per feature)
45
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
46
60
  ```
47
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
+
48
69
  **Decision:** `platform/` vs `shared/` is split by whether the code talks to
49
70
  a real external system.
50
71
  **Rationale:** Code that does I/O against Postgres/cache/queue/SMTP/S3 →
@@ -52,47 +73,80 @@ a real external system.
52
73
  add a `shared/utils` or similarly generic package — name it after what it
53
74
  actually does.
54
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
+
55
81
  ## 3. API Style
56
82
 
57
83
  **Decision:** REST over Gin, one handler package per domain, every route
58
84
  grouped under{{#if apiPrefix}} `/{{apiPrefix}}`{{else}} no prefix{{/if}}.
59
- **Rationale:** `handlerservicerepository`, one direction, all three
60
- 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
61
89
  single project-wide choice made at `create` time (`--api-prefix`) — there is
62
90
  no per-domain versioning; a domain that needs a real breaking change gets a
63
91
  new domain package (or a new field on the existing DTO), not a duplicated
64
92
  model pointed at the same table under a different URL.
65
93
 
66
- ## 4. Response and Error Model
94
+ ## 4. Application boundaries
95
+
96
+ **Decision:** New modules use the project defaults recorded in
97
+ `go-scaffold.config.json`: surface `{{defaultModuleSurface}}` and application
98
+ style `{{defaultApplicationStyle}}`. The `generate module` wizard presents
99
+ those values through Lean/CRUD/CQRS profiles, with Advanced for an explicit
100
+ custom combination. `--profile` or the supported axis flags can override the
101
+ defaults for one module.
102
+ **Rationale:** A feature with meaningful write invariants or a different read
103
+ shape benefits from separate `CommandHandler` and `QueryHandler` ports while
104
+ remaining inside this modular monolith. A small CRUD feature does not benefit
105
+ from empty command/query wrappers, so it keeps one service until its business
106
+ needs justify the split. CQRS here does not require a second database, broker,
107
+ or event-sourcing runtime.
108
+
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.
119
+
120
+ ## 5. Response and Error Model
67
121
 
68
122
  **Decision:** Central `apperror.AppError` (HTTP status + machine-readable
69
123
  `code` + message), rendered once by `middleware.Error()`.
70
124
  **Rationale:** Handlers only ever do `c.Error(err); return` — no `c.JSON` per
71
125
  call site, no error-shape drift between domains.
72
126
 
73
- - 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.
74
128
  `USER_NOT_FOUND`, not a bare `NOT_FOUND`)
75
129
  - DB errors are classified once in `shared/dberr` (`IsDuplicate`,
76
130
  `IsForeignKey`) and mapped to the right HTTP status per domain
77
131
 
78
- ## 5. Pagination
132
+ ## 6. Pagination
79
133
 
80
134
  **Decision:** Shared `limit`/`offset` parsing and response envelope
81
135
  (`shared/pagination`), used by every list endpoint.
82
136
  **Rationale:** One implementation, one response shape (`{data, limit,
83
137
  offset}`) — no per-domain reinvention.
84
138
 
85
- ## 6. Persistence
139
+ ## 7. Persistence
86
140
 
87
141
  **Decision:** PostgreSQL + GORM, schema managed by
88
142
  [golang-migrate](https://github.com/golang-migrate/migrate).
89
- **Rationale:** `AUTO_MIGRATE=true` runs GORM's AutoMigrate in dev for speed;
90
- prod runs `migrate up` as a separate, versioned, rollback-capable step —
91
- AutoMigrate is add-only and locks tables once there's real data.
143
+ **Rationale:** `APP_ENV=development` allows a convenience table bootstrap;
144
+ `APP_ENV=production` runs only after `migrate up` has applied the separate,
145
+ versioned, rollback-capable SQL migrations.
92
146
 
93
147
  {{#if docker}}- `docker-compose.yml` provides the local Postgres instance
94
148
  {{/if}}
95
- ## 7. IDs
149
+ ## 8. IDs
96
150
 
97
151
  **Decision:** UUID v7 for every entity, generated app-side
98
152
  (`shared/id.New()`), not by a DB default.
@@ -101,7 +155,7 @@ splits versus random v4 under heavy writes — and the app has the ID before
101
155
  insert, so it doesn't need `gen_random_uuid()`.
102
156
 
103
157
  {{#if openapiDocs}}
104
- ## 8. API Documentation
158
+ ## 9. API Documentation
105
159
 
106
160
  **Decision:** Hand-written OpenAPI spec (`docs/openapi.yaml`), split by
107
161
  domain module, read from the working copy only — the server serves no `/docs`
@@ -112,7 +166,7 @@ comment-generated (swaggo) if hand-updates start drifting.
112
166
  {{/if}}
113
167
 
114
168
  {{#if observability}}
115
- ## 9. Observability
169
+ ## 10. Observability
116
170
 
117
171
  **Decision:** Prometheus metrics (`GET /metrics`, request count + latency
118
172
  per route) always-on when this feature is enabled; OpenTelemetry tracing
@@ -147,5 +201,11 @@ skipped by the `os.Exit` calls already in there.
147
201
 
148
202
  - `go-scaffold generate module <name>` adds a new domain package and
149
203
  wires it into `cmd/api/wiring.go`
150
- - This document only reflects the initial scaffold — update it as the real
151
- 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.