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