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.
- package/README.md +138 -16
- package/bin/create-service.mjs +2 -0
- package/package.json +19 -11
- package/src/cli.test.ts +46 -7
- package/src/cli.ts +282 -84
- package/src/git-bootstrap.test.ts +40 -0
- package/src/git-bootstrap.ts +110 -0
- package/src/naming.test.ts +5 -2
- package/src/naming.ts +32 -1
- package/src/neon.ts +10 -8
- package/src/post-scaffold.test.ts +19 -0
- package/src/post-scaffold.ts +18 -26
- package/src/profiles.ts +25 -0
- package/src/scaffold.test.ts +320 -18
- package/src/scaffold.ts +154 -28
- package/src/vault.test.ts +94 -10
- package/src/vault.ts +81 -18
- package/templates/shared/.github/workflows/ci.yml +2 -1
- package/templates/shared/.github/workflows/deploy.yml +2 -0
- package/templates/shared/README.md +217 -29
- package/templates/shared/docker-compose.yml +19 -0
- package/templates/shared/grafana/alerts.yaml +54 -0
- package/templates/shared/grafana/waitlist-dashboard.json +63 -0
- package/templates/shared/scripts/authctl.ts +231 -0
- package/templates/shared/scripts/cloudrun/bootstrap.ts +24 -42
- package/templates/shared/scripts/cloudrun/cleanup.ts +81 -35
- package/templates/shared/scripts/cloudrun/cli.ts +324 -7
- package/templates/shared/scripts/cloudrun/config.ts +21 -19
- package/templates/shared/scripts/cloudrun/deploy.ts +16 -11
- package/templates/shared/scripts/cloudrun/lib.ts +232 -123
- package/templates/shared/scripts/cloudrun/neon.ts +127 -13
- package/templates/shared/scripts/dev.ts +22 -0
- package/templates/shared/scripts/ensure-local-db.ts +3 -0
- package/templates/shared/scripts/local-docker.ts +63 -0
- package/templates/shared/scripts/local-env.ts +27 -0
- package/templates/shared/scripts/seed.ts +73 -0
- package/templates/shared/scripts/wait-for-db.ts +32 -0
- package/templates/shared/service.config.ts +59 -0
- package/templates/shared/service.yaml +24 -1
- package/templates/targets/workers/.github/workflows/ci.yml +19 -0
- package/templates/targets/workers/.github/workflows/deploy.yml +19 -0
- package/templates/targets/workers/Makefile +33 -0
- package/templates/targets/workers/README.md +75 -0
- package/templates/targets/workers/package.json +35 -0
- package/templates/targets/workers/scripts/workers/cli.ts +397 -0
- package/templates/targets/workers/src/auth.ts +178 -0
- package/templates/targets/workers/src/index.ts +198 -0
- package/templates/targets/workers/src/storage.ts +370 -0
- package/templates/targets/workers/test/app.test.ts +108 -0
- package/templates/targets/workers/tsconfig.json +11 -0
- package/templates/targets/workers/wrangler.toml +24 -0
- package/templates/variants/bun-connectrpc/Dockerfile +1 -0
- package/templates/variants/bun-connectrpc/Makefile +17 -8
- package/templates/variants/bun-connectrpc/gen/protos/waitlist/v1/waitlist_pb.ts +424 -0
- package/templates/variants/bun-connectrpc/migrations/0000_init.sql +20 -0
- package/templates/variants/bun-connectrpc/package.json +25 -1
- package/templates/variants/bun-connectrpc/protos/waitlist/v1/waitlist.proto +91 -0
- package/templates/variants/bun-connectrpc/scripts/codegen.ts +31 -1
- package/templates/variants/bun-connectrpc/scripts/migrate.ts +49 -0
- package/templates/variants/bun-connectrpc/src/auth.ts +200 -0
- package/templates/variants/bun-connectrpc/src/db/client.ts +15 -0
- package/templates/variants/bun-connectrpc/src/db/repository.ts +126 -0
- package/templates/variants/bun-connectrpc/src/db/schema.ts +26 -0
- package/templates/variants/bun-connectrpc/src/index.ts +194 -22
- package/templates/variants/bun-connectrpc/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-connectrpc/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-connectrpc/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-connectrpc/src/waitlist/service.ts +172 -0
- package/templates/variants/bun-connectrpc/src/waitlist/types.ts +45 -0
- package/templates/variants/bun-connectrpc/test/app.test.ts +14 -13
- package/templates/variants/bun-connectrpc/test/waitlist.integration.test.ts +71 -0
- package/templates/variants/bun-connectrpc/tsconfig.json +2 -1
- package/templates/variants/bun-hono/Makefile +17 -8
- package/templates/variants/bun-hono/migrations/0000_init.sql +20 -0
- package/templates/variants/bun-hono/package.json +21 -1
- package/templates/variants/bun-hono/scripts/migrate.ts +49 -0
- package/templates/variants/bun-hono/src/auth.ts +181 -0
- package/templates/variants/bun-hono/src/db/client.ts +15 -0
- package/templates/variants/bun-hono/src/db/repository.ts +126 -0
- package/templates/variants/bun-hono/src/db/schema.ts +26 -0
- package/templates/variants/bun-hono/src/index.ts +141 -10
- package/templates/variants/bun-hono/src/temporal/activities.ts +14 -0
- package/templates/variants/bun-hono/src/temporal/worker.ts +38 -0
- package/templates/variants/bun-hono/src/temporal/workflows.ts +10 -0
- package/templates/variants/bun-hono/src/waitlist/service.ts +166 -0
- package/templates/variants/bun-hono/src/waitlist/types.ts +50 -0
- package/templates/variants/bun-hono/test/app.test.ts +90 -5
- package/templates/variants/bun-hono/test/waitlist.integration.test.ts +102 -0
- package/templates/variants/bun-hono/tsconfig.json +1 -0
- package/templates/variants/go-chi/Makefile +30 -10
- package/templates/variants/go-chi/atlas.hcl +8 -0
- package/templates/variants/go-chi/cmd/server/main.go +25 -13
- package/templates/variants/go-chi/go.mod +3 -2
- package/templates/variants/go-chi/internal/app/service.go +279 -70
- package/templates/variants/go-chi/internal/auth/middleware.go +289 -0
- package/templates/variants/go-chi/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-chi/internal/config/config.go +38 -7
- package/templates/variants/go-chi/internal/httpapi/routes.go +170 -47
- package/templates/variants/go-chi/internal/httpapi/waitlist_integration_test.go +199 -0
- package/templates/variants/go-chi/internal/temporal/activities.go +27 -0
- package/templates/variants/go-chi/internal/temporal/worker.go +42 -0
- package/templates/variants/go-chi/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-chi/migrations/0000_init.sql +20 -0
- package/templates/variants/go-chi/migrations/atlas.sum +2 -0
- package/templates/variants/go-chi/package.json +7 -1
- package/templates/variants/go-chi/test/go.test.ts +4 -1
- package/templates/variants/go-connectrpc/Makefile +29 -8
- package/templates/variants/go-connectrpc/atlas.hcl +8 -0
- package/templates/variants/go-connectrpc/buf.gen.yaml +2 -0
- package/templates/variants/go-connectrpc/cmd/server/main.go +44 -9
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlist.pb.go +960 -0
- package/templates/variants/go-connectrpc/gen/waitlist/v1/waitlistv1connect/waitlist.connect.go +283 -0
- package/templates/variants/go-connectrpc/go.mod +4 -0
- package/templates/variants/go-connectrpc/internal/app/service.go +279 -70
- package/templates/variants/go-connectrpc/internal/auth/middleware.go +289 -0
- package/templates/variants/go-connectrpc/internal/auth/middleware_test.go +38 -0
- package/templates/variants/go-connectrpc/internal/config/config.go +38 -7
- package/templates/variants/go-connectrpc/internal/connectapi/handler.go +129 -40
- package/templates/variants/go-connectrpc/internal/connectapi/waitlist_integration_test.go +122 -0
- package/templates/variants/go-connectrpc/internal/httpapi/routes.go +170 -47
- package/templates/variants/go-connectrpc/internal/temporal/activities.go +27 -0
- package/templates/variants/go-connectrpc/internal/temporal/worker.go +42 -0
- package/templates/variants/go-connectrpc/internal/temporal/workflows.go +18 -0
- package/templates/variants/go-connectrpc/migrations/0000_init.sql +20 -0
- package/templates/variants/go-connectrpc/migrations/atlas.sum +2 -0
- package/templates/variants/go-connectrpc/package.json +7 -1
- package/templates/variants/go-connectrpc/protos/waitlist/v1/waitlist.proto +93 -0
- package/templates/root/.github/workflows/buf-publish.yml +0 -19
- package/templates/root/.github/workflows/ci.yml +0 -26
- package/templates/root/.github/workflows/deploy.yml +0 -22
- package/templates/root/Dockerfile +0 -23
- package/templates/root/README.md +0 -69
- package/templates/root/buf.gen.yaml +0 -10
- package/templates/root/buf.yaml +0 -9
- package/templates/root/cmd/server/main.go +0 -44
- package/templates/root/gen/dns/v1/dns.pb.go +0 -623
- package/templates/root/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/root/go.mod +0 -10
- package/templates/root/internal/app/service.go +0 -152
- package/templates/root/internal/app/token_source.go +0 -50
- package/templates/root/internal/cloudflare/client.go +0 -160
- package/templates/root/internal/config/config.go +0 -55
- package/templates/root/internal/connectapi/handler.go +0 -79
- package/templates/root/internal/httpapi/routes.go +0 -93
- package/templates/root/internal/vault/client.go +0 -148
- package/templates/root/package.json +0 -12
- package/templates/root/protos/dns/v1/dns.proto +0 -58
- package/templates/root/scripts/cloudrun/bootstrap.ts +0 -65
- package/templates/root/scripts/cloudrun/config.ts +0 -50
- package/templates/root/scripts/cloudrun/deploy.ts +0 -41
- package/templates/root/scripts/cloudrun/lib.ts +0 -244
- package/templates/root/service.yaml +0 -50
- package/templates/root/test/go.test.ts +0 -19
- package/templates/shared/.env.example +0 -10
- package/templates/variants/go-chi/buf.gen.yaml +0 -10
- package/templates/variants/go-chi/buf.yaml +0 -9
- package/templates/variants/go-chi/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-chi/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- package/templates/variants/go-chi/internal/connectapi/handler.go +0 -79
- package/templates/variants/go-chi/protos/dns/v1/dns.proto +0 -58
- package/templates/variants/go-connectrpc/gen/dns/v1/dns.pb.go +0 -623
- package/templates/variants/go-connectrpc/gen/dns/v1/dnsv1connect/dns.connect.go +0 -192
- 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-
|
|
3
|
+
Generated by `create-service`.
|
|
4
4
|
|
|
5
|
-
This
|
|
5
|
+
This `{{PROFILE}}` profile targets `{{RUNTIME}} + {{FRAMEWORK}}` on Cloud Run with:
|
|
6
6
|
|
|
7
7
|
- one generated `service.yaml` manifest
|
|
8
|
-
- a
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
69
|
+
- the package-local CLI via `npx --no-install service ...`
|
|
38
70
|
|
|
39
|
-
|
|
71
|
+
Local provisioning intentionally prefers known-good CLIs over SDKs for Google Cloud operations.
|
|
40
72
|
|
|
41
|
-
|
|
73
|
+
Authenticate `gcloud` on the machine before running provisioning commands:
|
|
42
74
|
|
|
43
75
|
```bash
|
|
44
|
-
|
|
76
|
+
gcloud auth login
|
|
45
77
|
```
|
|
46
78
|
|
|
47
|
-
|
|
79
|
+
If you also want local Application Default Credentials available for other tools, run:
|
|
48
80
|
|
|
49
|
-
|
|
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
|
-
|
|
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 `
|
|
67
|
-
- `VAULT_NEON_API_KEY_FIELD` default `
|
|
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
|
-
|
|
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
|
+
}
|