create-svc 0.1.9 → 0.1.11

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 (163) hide show
  1. package/README.md +138 -16
  2. package/bin/create-service.mjs +2 -0
  3. package/package.json +19 -11
  4. package/src/cli.test.ts +46 -7
  5. package/src/cli.ts +282 -84
  6. package/src/git-bootstrap.test.ts +40 -0
  7. package/src/git-bootstrap.ts +110 -0
  8. package/src/naming.test.ts +5 -2
  9. package/src/naming.ts +32 -1
  10. package/src/neon.ts +10 -8
  11. package/src/post-scaffold.test.ts +19 -0
  12. package/src/post-scaffold.ts +18 -26
  13. package/src/profiles.ts +25 -0
  14. package/src/scaffold.test.ts +320 -18
  15. package/src/scaffold.ts +154 -28
  16. package/src/vault.test.ts +94 -10
  17. package/src/vault.ts +81 -18
  18. package/templates/shared/.github/workflows/ci.yml +2 -1
  19. package/templates/shared/.github/workflows/deploy.yml +2 -0
  20. package/templates/shared/README.md +217 -29
  21. package/templates/shared/docker-compose.yml +19 -0
  22. package/templates/shared/grafana/alerts.yaml +54 -0
  23. package/templates/shared/grafana/waitlist-dashboard.json +63 -0
  24. package/templates/shared/scripts/authctl.ts +231 -0
  25. package/templates/shared/scripts/cloudrun/bootstrap.ts +24 -42
  26. package/templates/shared/scripts/cloudrun/cleanup.ts +81 -35
  27. package/templates/shared/scripts/cloudrun/cli.ts +324 -7
  28. package/templates/shared/scripts/cloudrun/config.ts +21 -19
  29. package/templates/shared/scripts/cloudrun/deploy.ts +16 -11
  30. package/templates/shared/scripts/cloudrun/lib.ts +232 -123
  31. package/templates/shared/scripts/cloudrun/neon.ts +127 -13
  32. package/templates/shared/scripts/dev.ts +22 -0
  33. package/templates/shared/scripts/ensure-local-db.ts +3 -0
  34. package/templates/shared/scripts/local-docker.ts +63 -0
  35. package/templates/shared/scripts/local-env.ts +27 -0
  36. package/templates/shared/scripts/seed.ts +73 -0
  37. package/templates/shared/scripts/wait-for-db.ts +32 -0
  38. package/templates/shared/service.config.ts +59 -0
  39. package/templates/shared/service.yaml +24 -1
  40. package/templates/targets/workers/.github/workflows/ci.yml +19 -0
  41. package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
  42. package/templates/targets/workers/Makefile +33 -0
  43. package/templates/targets/workers/README.md +75 -0
  44. package/templates/targets/workers/package.json +35 -0
  45. package/templates/targets/workers/scripts/workers/cli.ts +397 -0
  46. package/templates/targets/workers/src/auth.ts +178 -0
  47. package/templates/targets/workers/src/index.ts +198 -0
  48. package/templates/targets/workers/src/storage.ts +370 -0
  49. package/templates/targets/workers/test/app.test.ts +108 -0
  50. package/templates/targets/workers/tsconfig.json +11 -0
  51. package/templates/targets/workers/wrangler.toml +24 -0
  52. package/templates/variants/bun-connectrpc/Dockerfile +1 -0
  53. package/templates/variants/bun-connectrpc/Makefile +17 -8
  54. package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
  55. package/templates/variants/bun-connectrpc/migrations/0000_init.sql +20 -0
  56. package/templates/variants/bun-connectrpc/package.json +25 -1
  57. package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
  58. package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
  59. package/templates/variants/bun-connectrpc/scripts/migrate.ts +49 -0
  60. package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
  61. package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
  62. package/templates/variants/bun-connectrpc/src/db/repository.ts +126 -0
  63. package/templates/variants/bun-connectrpc/src/db/schema.ts +26 -0
  64. package/templates/variants/bun-connectrpc/src/index.ts +194 -22
  65. package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
  66. package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
  67. package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
  68. package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
  69. package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
  70. package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
  71. package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
  72. package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
  73. package/templates/variants/bun-hono/Makefile +17 -8
  74. package/templates/variants/bun-hono/migrations/0000_init.sql +20 -0
  75. package/templates/variants/bun-hono/package.json +21 -1
  76. package/templates/variants/bun-hono/scripts/migrate.ts +49 -0
  77. package/templates/variants/bun-hono/src/auth.ts +181 -0
  78. package/templates/variants/bun-hono/src/db/client.ts +15 -0
  79. package/templates/variants/bun-hono/src/db/repository.ts +126 -0
  80. package/templates/variants/bun-hono/src/db/schema.ts +26 -0
  81. package/templates/variants/bun-hono/src/index.ts +141 -10
  82. package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
  83. package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
  84. package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
  85. package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
  86. package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
  87. package/templates/variants/bun-hono/test/app.test.ts +90 -5
  88. package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
  89. package/templates/variants/bun-hono/tsconfig.json +1 -0
  90. package/templates/variants/go-chi/Makefile +30 -10
  91. package/templates/variants/go-chi/atlas.hcl +8 -0
  92. package/templates/variants/go-chi/cmd/server/main.go +25 -13
  93. package/templates/variants/go-chi/go.mod +3 -2
  94. package/templates/variants/go-chi/internal/app/service.go +279 -70
  95. package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
  96. package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
  97. package/templates/variants/go-chi/internal/config/config.go +38 -7
  98. package/templates/variants/go-chi/internal/httpapi/routes.go +170 -47
  99. package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
  100. package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
  101. package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
  102. package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
  103. package/templates/variants/go-chi/migrations/0000_init.sql +20 -0
  104. package/templates/variants/go-chi/migrations/atlas.sum +2 -0
  105. package/templates/variants/go-chi/package.json +7 -1
  106. package/templates/variants/go-chi/test/go.test.ts +4 -1
  107. package/templates/variants/go-connectrpc/Makefile +29 -8
  108. package/templates/variants/go-connectrpc/atlas.hcl +8 -0
  109. package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
  110. package/templates/variants/go-connectrpc/cmd/server/main.go +44 -9
  111. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
  112. package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
  113. package/templates/variants/go-connectrpc/go.mod +4 -0
  114. package/templates/variants/go-connectrpc/internal/app/service.go +279 -70
  115. package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
  116. package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
  117. package/templates/variants/go-connectrpc/internal/config/config.go +38 -7
  118. package/templates/variants/go-connectrpc/internal/connectapi/handler.go +129 -40
  119. package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
  120. package/templates/variants/go-connectrpc/internal/httpapi/routes.go +170 -47
  121. package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
  122. package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
  123. package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
  124. package/templates/variants/go-connectrpc/migrations/0000_init.sql +20 -0
  125. package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
  126. package/templates/variants/go-connectrpc/package.json +7 -1
  127. package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
  128. package/templates/root/.github/workflows/buf-publish.yml +0 -19
  129. package/templates/root/.github/workflows/ci.yml +0 -26
  130. package/templates/root/.github/workflows/deploy.yml +0 -22
  131. package/templates/root/Dockerfile +0 -23
  132. package/templates/root/README.md +0 -69
  133. package/templates/root/buf.gen.yaml +0 -10
  134. package/templates/root/buf.yaml +0 -9
  135. package/templates/root/cmd/server/main.go +0 -44
  136. package/templates/root/gen/dns/v1/dns.pb.go +0 -623
  137. package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  138. package/templates/root/go.mod +0 -10
  139. package/templates/root/internal/app/service.go +0 -152
  140. package/templates/root/internal/app/token_source.go +0 -50
  141. package/templates/root/internal/cloudflare/client.go +0 -160
  142. package/templates/root/internal/config/config.go +0 -55
  143. package/templates/root/internal/connectapi/handler.go +0 -79
  144. package/templates/root/internal/httpapi/routes.go +0 -93
  145. package/templates/root/internal/vault/client.go +0 -148
  146. package/templates/root/package.json +0 -12
  147. package/templates/root/protos/dns/v1/dns.proto +0 -58
  148. package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
  149. package/templates/root/scripts/cloudrun/config.ts +0 -50
  150. package/templates/root/scripts/cloudrun/deploy.ts +0 -41
  151. package/templates/root/scripts/cloudrun/lib.ts +0 -244
  152. package/templates/root/service.yaml +0 -50
  153. package/templates/root/test/go.test.ts +0 -19
  154. package/templates/shared/.env.example +0 -10
  155. package/templates/variants/go-chi/buf.gen.yaml +0 -10
  156. package/templates/variants/go-chi/buf.yaml +0 -9
  157. package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
  158. package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  159. package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
  160. package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
  161. package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
  162. package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
  163. package/templates/variants/go-connectrpc/protos/dns/v1/dns.proto +0 -58
@@ -1,52 +1,96 @@
1
1
  # {{SERVICE_NAME}}
2
2
 
3
- Generated by `create-svc`.
3
+ Generated by `create-service`.
4
4
 
5
- This scaffold targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
5
+ This `{{PROFILE}}` profile targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
6
6
 
7
7
  - one generated `service.yaml` manifest
8
- - a local `svc-cloudrun` CLI for bootstrap, deploy, and cleanup
9
- - GitHub Actions for CI, `main` deploys, PR previews, and personal environments
10
- - GCP project bootstrap with billing and quota-project-aware `gcloud` calls
11
- - Neon main, preview, and personal branch provisioning
8
+ - a lightweight `{{EXAMPLE_LABEL}}` example surface
9
+ - local Docker Compose Postgres for first-run development
10
+ - a local `service` CLI for create, deploy, doctor, dashboards, and destroy
11
+ - GCP project create with billing and quota-project-aware `gcloud` calls
12
+ - Neon-backed remote database provisioning during create and deploy
13
+ - Better Auth client-credentials resource-server registration through `authctl`
14
+ - stage-aware waitlist data and trigger ingestion
15
+ - typed HTTP webhook ingestion where the selected template supports it
16
+ - a production API origin at `https://{{API_HOSTNAME}}`
17
+
18
+ The default happy path is standalone. Terraform is optional: advanced users can
19
+ precreate shared foundations and point this package at them, but a generated app
20
+ does not need Terraform state, Terraform plans, a control plane, or a platform
21
+ console to create and deploy.
12
22
 
13
23
  ## Commands
14
24
 
15
25
  ```bash
16
- make dev
17
- make gen
18
- make lint
19
- make test
20
- make bootstrap
21
- make deploy
22
- make deploy ARGS="--environment personal --name <slug>"
23
- make deploy ARGS="--destroy --environment personal --name <slug>"
24
- make cleanup
25
- make cleanup ARGS="--repo --project"
26
+ {{COMMAND_DEV}}
27
+ {{COMMAND_MIGRATE}}
28
+ {{COMMAND_GEN}}
29
+ {{COMMAND_LINT}}
30
+ {{COMMAND_TEST}}
31
+ {{COMMAND_BOOTSTRAP}}
32
+ {{COMMAND_DEPLOY}}
33
+ {{COMMAND_AUTH_RESOURCE}}
34
+ {{COMMAND_AUTH_CLIENT}}
35
+ {{COMMAND_DEPLOY_PERSONAL}}
36
+ {{COMMAND_DEPLOY_DESTROY}}
37
+ {{COMMAND_CLEANUP}}
38
+ {{COMMAND_CLEANUP_PROJECT}}
39
+ ```
40
+
41
+ ## Local development
42
+
43
+ The scaffold writes a ready-to-use `.env.local` and includes a local Postgres service in `docker-compose.yml`.
44
+
45
+ First local run:
46
+
47
+ ```bash
48
+ {{COMMAND_MIGRATE}}
49
+ {{COMMAND_DEV}}
26
50
  ```
27
51
 
28
- ## Configuration
52
+ Local runtime uses:
53
+
54
+ - `DATABASE_URL` from `.env.local`, pointed at Docker Compose Postgres
55
+ - `{{COMMAND_MIGRATE}}` and `{{COMMAND_DEV}}`, which open Docker Desktop if needed, wait for Docker readiness, and start Docker Compose Postgres
56
+ - `TEMPORAL_ENABLED=false` by default; set Temporal env vars locally only when you want to run against a real Temporal server
57
+
58
+ No cloud credentials are required for local HTTP development after Docker and Postgres are running.
59
+
60
+ ## Remote provisioning
29
61
 
30
62
  The generated Cloud Run config lives in [scripts/cloudrun/config.ts](scripts/cloudrun/config.ts).
31
63
 
32
- Bootstrap, deploy, and cleanup use:
64
+ Create, deploy, and destroy use:
33
65
 
66
+ - known-good CLIs first, especially `gcloud`
34
67
  - `gcloud`
35
- - `gh`
36
68
  - `NEON_API_KEY`, or a working Vault login via `VAULT_ADDR` plus `VAULT_TOKEN`, `VAULT_TOKEN_FILE`, or `~/.vault-token`
37
- - the local repo CLI via `npx --no-install svc-cloudrun ...`
69
+ - the package-local CLI via `npx --no-install service ...`
38
70
 
39
- ## Environment setup
71
+ Local provisioning intentionally prefers known-good CLIs over SDKs for Google Cloud operations.
40
72
 
41
- For project-specific Vault settings, prefer repo-local config over shell startup files:
73
+ Authenticate `gcloud` on the machine before running provisioning commands:
42
74
 
43
75
  ```bash
44
- cp .env.example .env.local
76
+ gcloud auth login
45
77
  ```
46
78
 
47
- Then edit `.env.local` with your Vault address and secret path overrides.
79
+ If you also want local Application Default Credentials available for other tools, run:
48
80
 
49
- For the token itself, prefer a normal Vault login flow:
81
+ ```bash
82
+ gcloud auth application-default login
83
+ ```
84
+
85
+ The generated backend scripts still use `gcloud` as the primary control plane even when ADC is present.
86
+
87
+ Go variants use Atlas for migrations:
88
+
89
+ ```bash
90
+ atlas version
91
+ ```
92
+
93
+ For the Neon admin credential, prefer a normal Vault login flow:
50
94
 
51
95
  ```bash
52
96
  vault login
@@ -60,10 +104,154 @@ The scaffold will use, in order:
60
104
 
61
105
  That keeps stable settings in the repo and keeps the token out of `~/.zshrc`.
62
106
 
63
- Optional Vault overrides for Neon admin key lookup:
107
+ For production auth registration, `authctl` also needs the auth service's
108
+ Cloudflare Access service token:
109
+
110
+ ```bash
111
+ export AUTH_INTERNAL_BASE_URL="$(vault kv get -mount=secret -field=AUTH_INTERNAL_BASE_URL prod/apps/auth/authctl/cloudflare-access)"
112
+ export CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID="$(vault kv get -mount=secret -field=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_ID prod/apps/auth/authctl/cloudflare-access)"
113
+ export CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET="$(vault kv get -mount=secret -field=CLOUDFLARE_ACCESS_SERVICE_TOKEN_CLIENT_SECRET prod/apps/auth/authctl/cloudflare-access)"
114
+ ```
115
+
116
+ Before first production create, verify the installed `authctl` exposes the
117
+ resource-server control-plane command:
118
+
119
+ ```bash
120
+ {{COMMAND_AUTH_RESOURCE}} --help
121
+ ```
122
+
123
+ If this fails with `authctl is installed but does not expose resource-server
124
+ commands`, make sure the generated package installed `@anmho/authctl@0.1.1` or
125
+ newer before running `{{COMMAND_BOOTSTRAP}}`.
126
+
127
+ Optional remote-only Vault overrides for Neon admin key lookup:
64
128
 
65
129
  - `VAULT_SECRET_MOUNT` default `secret`
66
- - `VAULT_NEON_API_KEY_PATH` default `provider/neon-api-key`
67
- - `VAULT_NEON_API_KEY_FIELD` default `value`
130
+ - `VAULT_NEON_API_KEY_PATH` default `prod/providers/neon`
131
+ - `VAULT_NEON_API_KEY_FIELD` default `api_key`
132
+
133
+ The generator only stores application-facing connection material in Secret Manager. Neon admin credentials stay local to create and deploy.
134
+
135
+ ## Temporal
136
+
137
+ Cloud Run variants include an in-process Temporal worker in the service process.
138
+ Production Temporal is enabled when you set `TEMPORAL_ENABLED=true`, or when
139
+ `TEMPORAL_ADDRESS`, `TEMPORAL_API_KEY`, or `TEMPORAL_API_KEY_SECRET` is present
140
+ during deploy rendering.
141
+
142
+ For Temporal Cloud, provide:
143
+
144
+ ```bash
145
+ TEMPORAL_ENABLED=true
146
+ TEMPORAL_ADDRESS=<namespace>.<account>.tmprl.cloud:7233
147
+ TEMPORAL_NAMESPACE=<namespace>.<account>
148
+ TEMPORAL_API_KEY=<one-time local value for service create>
149
+ ```
150
+
151
+ `service create` writes `TEMPORAL_API_KEY` to Secret Manager as
152
+ `{{SERVICE_ID}}-temporal-api-key` and grants the runtime service account access.
153
+ Later deploys can set `TEMPORAL_API_KEY_SECRET={{SERVICE_ID}}-temporal-api-key`
154
+ without exposing the key locally.
155
+
156
+ ## Service auth
157
+
158
+ Generated services are resource servers for the Better Auth client-credentials
159
+ server at `https://auth.anmho.com`.
160
+
161
+ `service create` registers this service as an auth resource server through
162
+ `authctl` before the first production deploy. The generated resource server is:
163
+
164
+ - id: `{{SERVICE_ID}}`
165
+ - audience: `api://{{SERVICE_ID}}`
166
+ - default scopes: `{{SERVICE_ID}}:read`, `{{SERVICE_ID}}:write`
167
+
168
+ Production runtime has `AUTH_ENABLED=true` and verifies JWT bearer tokens from
169
+ the Better Auth JWKS endpoint on `/v1/*` and ConnectRPC service paths. Local
170
+ development defaults to `AUTH_ENABLED=false` in `.env.local`.
171
+
172
+ Use `service auth` for follow-up auth operations:
173
+
174
+ ```bash
175
+ {{COMMAND_BOOTSTRAP}} # includes resource-server registration
176
+ {{COMMAND_AUTH_RESOURCE}}
177
+ {{COMMAND_AUTH_CLIENT}} --resource-server <target-service> --scope <scope>
178
+ ```
179
+
180
+ `authctl clients create` prints a one-time client secret plus the recommended
181
+ Vault command. By convention, generated services store outgoing service-client
182
+ credentials under:
183
+
184
+ ```text
185
+ prod/apps/{{SERVICE_ID}}/server/oauth-clients/<resource_server_id>
186
+ ```
187
+
188
+ When requesting a client-credentials token for a generated service, include the
189
+ target resource server as `resource=api://<resource_server_id>`. The generated
190
+ runtime expects a JWT with that audience; omitting `resource` can return an
191
+ opaque access token that the service will reject.
192
+
193
+ Webhook signature hooks are provider-specific and optional in v1. Add provider
194
+ secrets only when you add a provider adapter. A generic adapter can honor:
195
+
196
+ - `WEBHOOK_<PROVIDER>_SECRET`
197
+
198
+ ## One-command production create
199
+
200
+ The one-command production create path is designed for a fresh standalone service.
201
+
202
+ When generated through `create-service`, the intended flow is:
203
+
204
+ ```bash
205
+ bun create service {{SERVICE_NAME}} --yes
206
+ ```
207
+
208
+ That command scaffolds this package, runs `service create`, deploys the production
209
+ Cloud Run service through `service deploy`, and fails loudly with resumable
210
+ instructions if a required cloud credential is missing. The generated package can also be run
211
+ manually:
212
+
213
+ ```bash
214
+ {{COMMAND_BOOTSTRAP}}
215
+ {{COMMAND_DEPLOY}}
216
+ ```
217
+
218
+ Create reads Neon credentials from environment variables or from Vault when
219
+ `VAULT_ADDR` and a Vault token are available. Runtime database credentials are
220
+ delivered to Cloud Run through app-project Secret Manager.
221
+
222
+ ## Production API
223
+
224
+ The main environment is intended to be public at:
225
+
226
+ ```bash
227
+ https://{{API_HOSTNAME}}
228
+ ```
229
+
230
+ Preview and personal environments keep using deterministic Cloud Run hostnames in v1 so the generated backend stays easy to use inside standalone repos or monorepos.
231
+
232
+ ## Generated backend domain
233
+
234
+ The generated microservice profile is a waitlist/launch service example. It is
235
+ kept deliberately small so the integration plumbing is easy to remove or adapt:
236
+
237
+ - public launch/waitlist submission
238
+ - status lookup
239
+ - trigger ingestion for scheduled, webhook, and manual follow-up work
240
+ - provider webhook ingress where the selected template supports it
241
+
242
+ The current Hono backend plumbing includes:
243
+
244
+ - `waitlist_entries`
245
+ - `waitlist_triggers`
246
+
247
+ Hono variants expose:
248
+
249
+ - `POST /v1/waitlist`
250
+ - `GET /v1/waitlist?email=...`
251
+ - `GET /v1/waitlist/{entryId}`
252
+ - `POST /v1/triggers/waitlist`
253
+ - `POST /webhooks/:provider`
68
254
 
69
- The generator only stores application-facing connection material in Secret Manager. Neon admin credentials stay local to bootstrap and deploy.
255
+ ConnectRPC variants expose typed unary waitlist RPCs and will usually be the
256
+ first place to adapt domain-specific contracts after scaffold.
257
+ {{LOCAL_INTROSPECTION_NOTE}}
@@ -0,0 +1,19 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ environment:
5
+ POSTGRES_DB: {{LOCAL_DATABASE_NAME}}
6
+ POSTGRES_USER: {{LOCAL_DATABASE_USER}}
7
+ POSTGRES_PASSWORD: {{LOCAL_DATABASE_PASSWORD}}
8
+ ports:
9
+ - "127.0.0.1:{{LOCAL_DATABASE_PORT}}:5432"
10
+ volumes:
11
+ - postgres-data:/var/lib/postgresql/data
12
+ healthcheck:
13
+ test: ["CMD-SHELL", "pg_isready -U {{LOCAL_DATABASE_USER}} -d {{LOCAL_DATABASE_NAME}}"]
14
+ interval: 5s
15
+ timeout: 5s
16
+ retries: 10
17
+
18
+ volumes:
19
+ postgres-data:
@@ -0,0 +1,54 @@
1
+ apiVersion: 1
2
+ groups:
3
+ - orgId: 1
4
+ name: "{{SERVICE_NAME}} baseline"
5
+ folder: "create-service"
6
+ interval: 1m
7
+ rules:
8
+ - uid: "{{SERVICE_ID}}-high-error-rate"
9
+ title: "{{SERVICE_NAME}} high error rate"
10
+ condition: C
11
+ data:
12
+ - refId: A
13
+ relativeTimeRange:
14
+ from: 600
15
+ to: 0
16
+ datasourceUid: "${datasource}"
17
+ model:
18
+ expr: "sum(rate(http_requests_total{service=\"{{SERVICE_NAME}}\",status=~\"5..\"}[5m]))"
19
+ intervalMs: 1000
20
+ maxDataPoints: 43200
21
+ refId: A
22
+ - refId: C
23
+ relativeTimeRange:
24
+ from: 600
25
+ to: 0
26
+ datasourceUid: __expr__
27
+ model:
28
+ conditions:
29
+ - evaluator:
30
+ params: [0.05]
31
+ type: gt
32
+ operator:
33
+ type: and
34
+ query:
35
+ params: [A]
36
+ reducer:
37
+ type: last
38
+ type: query
39
+ datasource:
40
+ type: __expr__
41
+ uid: __expr__
42
+ expression: A
43
+ intervalMs: 1000
44
+ maxDataPoints: 43200
45
+ refId: C
46
+ type: threshold
47
+ noDataState: NoData
48
+ execErrState: Error
49
+ for: 5m
50
+ annotations:
51
+ summary: "{{SERVICE_NAME}} 5xx rate is elevated"
52
+ labels:
53
+ service_id: "{{SERVICE_ID}}"
54
+ target: "{{TARGET}}"
@@ -0,0 +1,63 @@
1
+ {
2
+ "uid": "{{SERVICE_ID}}-waitlist",
3
+ "title": "{{SERVICE_NAME}} Waitlist Service",
4
+ "tags": ["create-service", "{{TARGET}}", "{{RUNTIME}}", "{{FRAMEWORK}}"],
5
+ "timezone": "browser",
6
+ "schemaVersion": 39,
7
+ "version": 1,
8
+ "refresh": "30s",
9
+ "panels": [
10
+ {
11
+ "id": 1,
12
+ "type": "timeseries",
13
+ "title": "Request rate",
14
+ "datasource": { "type": "prometheus", "uid": "${datasource}" },
15
+ "targets": [
16
+ {
17
+ "refId": "A",
18
+ "expr": "sum(rate(http_requests_total{service=\"{{SERVICE_NAME}}\"}[5m]))"
19
+ }
20
+ ],
21
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }
22
+ },
23
+ {
24
+ "id": 2,
25
+ "type": "timeseries",
26
+ "title": "Error rate",
27
+ "datasource": { "type": "prometheus", "uid": "${datasource}" },
28
+ "targets": [
29
+ {
30
+ "refId": "A",
31
+ "expr": "sum(rate(http_requests_total{service=\"{{SERVICE_NAME}}\",status=~\"5..\"}[5m]))"
32
+ }
33
+ ],
34
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }
35
+ },
36
+ {
37
+ "id": 3,
38
+ "type": "timeseries",
39
+ "title": "p95 latency",
40
+ "datasource": { "type": "prometheus", "uid": "${datasource}" },
41
+ "targets": [
42
+ {
43
+ "refId": "A",
44
+ "expr": "histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{service=\"{{SERVICE_NAME}}\"}[5m])) by (le))"
45
+ }
46
+ ],
47
+ "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }
48
+ },
49
+ {
50
+ "id": 4,
51
+ "type": "stat",
52
+ "title": "Queued triggers",
53
+ "datasource": { "type": "prometheus", "uid": "${datasource}" },
54
+ "targets": [
55
+ {
56
+ "refId": "A",
57
+ "expr": "sum(waitlist_triggers_queued{service=\"{{SERVICE_NAME}}\"})"
58
+ }
59
+ ],
60
+ "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }
61
+ }
62
+ ]
63
+ }
@@ -0,0 +1,231 @@
1
+ import serviceConfig from "../service.config";
2
+ import { existsSync } from "node:fs";
3
+
4
+ type CommandResult = {
5
+ success: boolean;
6
+ stdout: string;
7
+ stderr: string;
8
+ exitCode: number;
9
+ };
10
+
11
+ const decoder = new TextDecoder();
12
+
13
+ export type AuthDoctorResult = {
14
+ hasAuthctl: boolean;
15
+ hasResourceServerCommands: boolean;
16
+ detail: string;
17
+ resourceServerCommand?: ResourceServerCommand;
18
+ };
19
+
20
+ type ResourceServerCommand = {
21
+ subject: string;
22
+ mutationAction?: "upsert" | "create";
23
+ actions: string[];
24
+ };
25
+
26
+ type ResourceServerMutationCommand = ResourceServerCommand & {
27
+ mutationAction: "upsert" | "create";
28
+ };
29
+
30
+ export function defaultAuthResourceServerArgs() {
31
+ const auth = serviceConfig.auth;
32
+ return [
33
+ "--resource-server",
34
+ auth.resource_server.id,
35
+ "--audience",
36
+ auth.resource_server.audience,
37
+ "--stage",
38
+ serviceConfig.stage_default,
39
+ ...auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope]),
40
+ ];
41
+ }
42
+
43
+ export function runAuthCommand(args: string[]) {
44
+ const [subject, action, ...rest] = args;
45
+
46
+ if (!subject || subject === "doctor") {
47
+ const result = runAuthDoctor();
48
+ if (!result.hasAuthctl) {
49
+ throw new Error(result.detail);
50
+ }
51
+ return result.detail;
52
+ }
53
+
54
+ if (subject === "resource-server" || subject === "resource-servers") {
55
+ const command = resolveResourceServerCommand();
56
+ if (!command) {
57
+ throw new Error(
58
+ "authctl is installed but does not expose resource-server commands; install @anmho/authctl@0.1.1 or newer before managing auth resource servers"
59
+ );
60
+ }
61
+ if (action === "get" || action === "list") {
62
+ if (!command.actions.includes(action)) {
63
+ throw new Error(`authctl ${command.subject} does not expose ${action}`);
64
+ }
65
+ authctl([command.subject, action, ...rest]);
66
+ return `Auth resource server ${action} finished`;
67
+ }
68
+ const mutation = ensureResourceServerCommandAvailable();
69
+ const subcommand = action ?? mutation.mutationAction;
70
+ if (!mutation.mutationAction || (subcommand !== mutation.mutationAction && !(subcommand === "upsert" && mutation.mutationAction === "create"))) {
71
+ throw new Error(`Usage: service auth resource-server [${mutation.mutationAction}] [authctl args]`);
72
+ }
73
+ authctl([mutation.subject, mutation.mutationAction, ...defaultAuthResourceServerArgs(), "--json", ...rest]);
74
+ return `Auth resource server ready: ${serviceConfig.auth.resource_server.id}`;
75
+ }
76
+
77
+ if (subject === "client" || subject === "clients") {
78
+ return runClientCommand(action, rest);
79
+ }
80
+
81
+ throw new Error("Usage: service auth <doctor|resource-server|client> [args]");
82
+ }
83
+
84
+ export function ensureAuthResourceServer() {
85
+ const command = ensureResourceServerCommandAvailable();
86
+ authctl([command.subject, command.mutationAction, ...defaultAuthResourceServerArgs(), "--json"]);
87
+ return `Auth resource server ready: ${serviceConfig.auth.resource_server.audience}`;
88
+ }
89
+
90
+ export function runAuthDoctor(): AuthDoctorResult {
91
+ if (!authctlPath()) {
92
+ return {
93
+ hasAuthctl: false,
94
+ hasResourceServerCommands: false,
95
+ detail: "authctl is not installed; run bun install in this generated service or link @anmho/authctl before service create",
96
+ };
97
+ }
98
+
99
+ const doctor = authctl(["doctor", "--json"], { allowFailure: true, quiet: true });
100
+ const resourceServerCommand = resolveResourceServerCommand();
101
+ const hasResourceServerCommands = Boolean(resourceServerCommand?.mutationAction);
102
+
103
+ if (!doctor.success) {
104
+ return {
105
+ hasAuthctl: true,
106
+ hasResourceServerCommands,
107
+ resourceServerCommand,
108
+ detail: `authctl doctor failed: ${doctor.stderr || doctor.stdout}`,
109
+ };
110
+ }
111
+
112
+ if (!hasResourceServerCommands) {
113
+ return {
114
+ hasAuthctl: true,
115
+ hasResourceServerCommands: false,
116
+ resourceServerCommand,
117
+ detail:
118
+ "authctl is installed but does not expose resource-server upsert/create; install @anmho/authctl@0.1.1 or newer before service create",
119
+ };
120
+ }
121
+
122
+ return {
123
+ hasAuthctl: true,
124
+ hasResourceServerCommands: true,
125
+ resourceServerCommand,
126
+ detail: `authctl ready for ${serviceConfig.auth.resource_server.id}`,
127
+ };
128
+ }
129
+
130
+ function runClientCommand(action = "", rest: string[]) {
131
+ if (action === "create") {
132
+ authctl([
133
+ "clients",
134
+ "create",
135
+ "--client-app",
136
+ serviceConfig.auth.client.app_id,
137
+ "--client-identity",
138
+ serviceConfig.auth.client.identity,
139
+ ...defaultClientTargetArgs(rest),
140
+ "--stage",
141
+ serviceConfig.stage_default,
142
+ "--yes",
143
+ "--json",
144
+ ...rest,
145
+ ]);
146
+ return "Auth client created";
147
+ }
148
+
149
+ if (["list", "get", "rotate", "revoke"].includes(action)) {
150
+ authctl(["clients", action, ...rest]);
151
+ return `Auth client ${action} finished`;
152
+ }
153
+
154
+ throw new Error("Usage: service auth client <create|list|get|rotate|revoke> [args]");
155
+ }
156
+
157
+ function defaultClientTargetArgs(rest: string[]) {
158
+ const hasResourceServer = hasFlag(rest, "--resource-server");
159
+ const hasScope = hasFlag(rest, "--scope");
160
+ return [
161
+ ...(hasResourceServer ? [] : ["--resource-server", serviceConfig.auth.resource_server.id]),
162
+ ...(hasScope ? [] : serviceConfig.auth.resource_server.default_scopes.flatMap((scope) => ["--scope", scope])),
163
+ ];
164
+ }
165
+
166
+ function hasFlag(args: string[], name: string) {
167
+ return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
168
+ }
169
+
170
+ function ensureResourceServerCommandAvailable(): ResourceServerMutationCommand {
171
+ const doctor = runAuthDoctor();
172
+ if (!doctor.hasAuthctl || !doctor.hasResourceServerCommands) {
173
+ throw new Error(doctor.detail);
174
+ }
175
+ if (!doctor.resourceServerCommand?.mutationAction) {
176
+ throw new Error("authctl resource-server command discovery failed");
177
+ }
178
+ return doctor.resourceServerCommand as ResourceServerMutationCommand;
179
+ }
180
+
181
+ function resolveResourceServerCommand(): ResourceServerCommand | undefined {
182
+ for (const subject of ["resource-servers", "resource-server", "resources"]) {
183
+ const help = authctl([subject, "--help"], { allowFailure: true, quiet: true });
184
+ const output = `${help.stdout}\n${help.stderr}`;
185
+ if (!help.success || !output.includes(subject)) {
186
+ continue;
187
+ }
188
+ const actions = ["upsert", "create", "get", "list"].filter((candidate) => output.includes(candidate));
189
+ const mutationAction = actions.includes("upsert") ? "upsert" : actions.includes("create") ? "create" : undefined;
190
+ if (actions.length > 0) {
191
+ return { subject, mutationAction, actions };
192
+ }
193
+ }
194
+ return undefined;
195
+ }
196
+
197
+ function authctl(args: string[], options: { allowFailure?: boolean; quiet?: boolean } = {}): CommandResult {
198
+ const command = authctlPath();
199
+ if (!command) {
200
+ throw new Error("authctl is not installed; run bun install in this generated service or link @anmho/authctl");
201
+ }
202
+
203
+ const result = Bun.spawnSync([command, ...args], {
204
+ cwd: process.cwd(),
205
+ env: process.env,
206
+ stdin: "inherit",
207
+ stdout: "pipe",
208
+ stderr: "pipe",
209
+ });
210
+
211
+ const output = {
212
+ success: result.success,
213
+ stdout: result.stdout ? decoder.decode(result.stdout).trim() : "",
214
+ stderr: result.stderr ? decoder.decode(result.stderr).trim() : "",
215
+ exitCode: result.exitCode,
216
+ };
217
+
218
+ if (!output.success && !options.allowFailure) {
219
+ throw new Error(`authctl ${args.join(" ")} failed with exit code ${output.exitCode}\n${output.stderr || output.stdout}`);
220
+ }
221
+
222
+ if (output.stdout && !options.quiet) {
223
+ console.log(output.stdout);
224
+ }
225
+
226
+ return output;
227
+ }
228
+
229
+ function authctlPath() {
230
+ return existsSync("./node_modules/.bin/authctl") ? "./node_modules/.bin/authctl" : Bun.which("authctl");
231
+ }