v0.8.0
Released: October 2, 2026
This release is about the gateway. API keys are now real runtime data: you create, limit, rotate and revoke them through the auth-service, and the gateway enforces them on every request with no redeploy. Along the way we layered the trust between the gateway and your services, gave users roles, and made the gateway image the same everywhere. It has breaking changes; the upgrade guide at the end walks through them in order.
API keys you manage at runtime
Partner keys used to be static: a secret in .secrets.user.json, a consumer in kong.user.yml, and a gateway rebuild for every new partner or rotation. That is gone. Keys now live in Postgres, with a copy in Redis that the gateway reads.
- Admins create keys for a named consumer (
acme-corp), not a user, through new admin endpoints on the auth-service. The key is shown once and stored only as a hash. - Each key can have its own limits per minute, hour, day, week and month, and an optional expiry. A trial is just a key with low limits and a near expiry.
- Rotation issues a new key and keeps the old one working for a grace period (7 days by default). Revocation takes effect on the next request.
- The global rate limit in
kong.user.ymlbecomes the default for every key, counted per key. A key's own limits replace it, higher or lower. - A per-IP ceiling on API key routes (600 requests per minute by default,
framework.apiKeys.ipLimitPerMinutein.tsdevstack/config.json) slows down anyone guessing keys. - Clear error codes from the gateway:
api_key_missing,invalid_api_key,api_key_revoked,api_key_expired,rate_limit_exceeded,quota_exceeded, and 503index_unavailableorgateway_unavailablewhen keys cannot be checked. Validation fails closed. - A scheduled job,
sync-api-key-usage, saves week and month usage to Postgres and rebuilds the Redis copy if Redis loses its data. - Projects without the auth template can write keys to Redis with their own tooling. The record format is a documented, versioned contract, and
@tsdevstack/nest-commonexports it as code.
Everything is in the new API Keys page.
Exact routes at the gateway
The gateway now routes only what your OpenAPI document declares: the exact path and its declared methods, plus OPTIONS for CORS preflights. Everything else gets 404 from the gateway before it reaches a service: unknown paths, extra path segments, a trailing slash, undeclared methods (including HEAD on a GET route), and endpoints hidden from OpenAPI.
Partner routes use the same model: each @PartnerApi() operation gets its own route at /api plus its path. See Gateway Routing.
Token checks got faster too: the gateway caches the auth-service's OIDC discovery document and signing keys for 24 hours instead of fetching them per request. New signing keys are picked up right away.
Gateway and service trust, layered
- A new built-in gateway plugin,
tsdevstack-strip-identity, clears identity headers (X-Userinfo,X-Consumer-*,X-Api-Key-*,X-Kong-Trustand similar) on every incoming request, before authentication runs. Only the gateway itself sets them. AuthGuardchecks the gateway's trust token first, then the identity headers. Without a valid token, identity headers are ignored: the request is an internal service call with the serviceAPI_KEY, an anonymous call to a@Public()handler, or 401.- A partner key only works on
@PartnerApi()handlers, and gets 403 anywhere else,@Public()handlers included.
Details in Protected Routes.
Roles and managing users
- Every user has a system role (
USERorADMIN) and any number of custom roles you declare. Restrict any endpoint with@Roles()from nest-common. - The first admin comes from a new
ADMIN_EMAILSsecret: a confirmed user listed there becomes admin at login.generate-secretsadds it to your.secrets.user.json, andcloud-secrets:pushoffers it. - Admin endpoints to find users and change their roles, and a change-password endpoint for logged-in users.
See Roles and Managing Users.
One gateway image, locally and in the cloud
The gateway image is now generated from one definition for local development and every cloud provider. infrastructure/kong/Dockerfile and .dockerignore are generated files. Your own Lua plugins go in a kong-plugins/ folder at the project root and end up in both the local and the cloud image. See Kong Plugins.
AWS: scale-to-zero retired
We retired scale-to-zero on AWS to keep the setup simple. Waking a sleeping service on ECS took extra moving parts and was not reliable enough. Services and the gateway now always run at least one task and scale on CPU between minInstances and maxInstances.
This costs more: every AWS service keeps at least one task running. The AWS cost estimates are updated. GCP and Azure keep minInstances: 0.
init uses templates that match your CLI
init now clones the templates tagged with your CLI's version, so a new project always gets templates that work with the packages it installs. If the tag does not exist (an unreleased build), it falls back to the latest templates with a warning.
Smaller fixes
- Rate limiting and health checks recover after a Redis interruption. The shared Redis client used to give up after a few retries and needed a service restart.
- Regenerated CI workflows print the current gateway commands.
- Environment password protection was removed a while ago, but its config fields were still accepted and silently ignored. They are now rejected.
- AWS deployment fixes: scheduled job authentication, Next.js assets through CloudFront, migration status reporting.
- Single-service deploys failed with a false "topology changed" error in projects with a worker.
- Commands no longer fail with "Backend configuration changed" after an environment moves to a new project or provider.
- Azure:
cloud-secrets:listshows service-scoped secrets. SecretsService.getProvider().getAll()returns only shared secrets and the service's own.- Clearer hints in
generate-kongand for missing images. - New on Azure:
database.serverNamesets the PostgreSQL server name, for when the default name is taken. See Custom server name.
Breaking changes
Pre-1.0, so they ship in a minor version. Each one is covered by the upgrade guide below.
API keys
- Static partner keys no longer work. Consumers with
keyauth_credentialsinkong.user.ymlstay in the merged config, but no generated route checks them;generate-kongandinfra:generate-kongwarn while they are there. They stop working when you regenerate and redeploy the gateway. - Partner keys are not secrets anymore. Old partner key values in
.secrets.user.jsonand your cloud secret manager can go. - With the auth template, every cloud environment needs the
sync-api-key-usagescheduled job.infra:generatewarns and prints the entry until it is there. - The auth-service needs Redis 7.0 or later. Every generated environment and the local compose file already run 7.x.
- An admin who created API keys cannot be deleted from the database while those keys exist.
Gateway
- Undeclared paths, methods, trailing slashes and extra segments get 404 from the gateway.
HEADis not added toGETroutes. - Endpoints missing from OpenAPI (
@ApiExcludeEndpoint(),@ApiExcludeController(), raw Express routes, static files) are unreachable through the gateway. That is intended for jobs, health checks and metrics; anything else must be in the OpenAPI document. - NestJS wildcard (
*splat) and optional ({/:id}) routes appear in OpenAPI as a single segment or one variant, so the gateway now routes only that. Declare the variants you need as separate routes. - Partner routes exist only for
@PartnerApi()operations. The old catch-all/api/{prefix}route is gone. - Gateway route names changed (
{service}-{public|jwt|partner}-{path}). Anything that referred to the old names breaks. infrastructure/kong/Dockerfileand.dockerignoreare generated and overwritten bygenerate-kong. Custom plugins move tokong-plugins/.- A fresh clone needs
npx tsdevstack sync(orgenerate-kong) beforedocker compose up, because parts of the gateway build context are generated.docker compose upwithout--buildkeeps the old gateway image, which cannot start with the new config. - The
request-transformerinkong.user.ymlmust only handleX-Kong-TrustandX-Kong-Request-Id. Older files also removeX-Consumer-*andX-JWT-Claim-*headers; the generators warn until you delete those entries. - A full
kong.custom.ymlmust now declare the framework plugins itself: the globaltsdevstack-strip-identity, andtsdevstack-api-key, a per-IPrate-limitingandtsdevstack-api-prefixon partner services. - Services without a
globalPrefixnow use the same prefix rule locally as in the cloud (the service name without-service). - Locally, the gateway reads the client IP from
X-Forwarded-For, the same as behind a cloud load balancer. - The gateway image sets
KONG_NGINX_HTTP_INCLUDEfor its OIDC caches. If you set it on the gateway container yourself, you replace the framework's include and the caches stop working. OtherKONG_NGINX_HTTP_*settings are unaffected. - The gateway keeps the auth-service's signing keys for up to 24 hours. If you ever remove a signing key in an emergency, redeploy the gateway (
infra:deploy-kong) so it stops accepting tokens signed with it.
Backends (nest-common)
- Requests that did not come through the gateway get 401 on protected handlers when they carry identity headers. That includes calling
localhost:300xdirectly with a bearer token, Swagger UI's "Try it out" on protected endpoints, end-to-end tests against service ports and sidecars. Go through the gateway, or use the serviceAPI_KEYfor internal calls. - Partner requests never set
req.user. They setreq.authType === 'apiKey',req.apiKey = { id, consumer }andreq.service === 'partner';@Partner()returns the consumer name. Internal calls that forward a partner's headers count as partner calls downstream, so the downstream handler needs@PartnerApi()too. - A wrong service
x-api-keyon a@Public()handler is ignored instead of rejected. A trusted request with anx-api-keybut no gateway identity is checked against the serviceAPI_KEY. KongHeaders.JWT_CLAIM_PREFIX,KongHeaders.CONSUMER_ID,KongHeaders.CONSUMER_USERNAMEandKongHeaders.CREDENTIAL_IDENTIFIERare removed, andX-JWT-Claim-*headers are no longer read. Read claims fromreq.user.RateLimitGuarduses new Redis key names, so rate-limit counters reset on deploy. Per-IP limits use the gateway'sX-Real-IPonly for requests that came through the gateway, and neverX-Forwarded-For.- With the Redis health check enabled (
HealthModule.forRoot({ redis: true })),/healthreports Redisdownduring an outage (overall statusdegraded, still HTTP 200), so monitoring may alert where it stayed quiet before.
Auth template
- The user's
rolefield is nowsystemRole(enumUserTypeis nowSystemRole), next to a newroleslist. Tokens carrysystemRoleandrolesinstead ofrole, andUserDtochanged the same way, so a regenerated client breaks frontend code that readsuser.role.@Roles()still accepts older tokens with aroleclaim. - A plain
prisma migrate devafter copying the new schema drops therolecolumn and resets everyone toUSER. Use the rename migration from the upgrade guide instead. PUT /v1/user/passwordrevokes every refresh token of the user and returns a new token pair; a frontend must store it or its session ends at the next refresh.
AWS
minInstances: 0is rejected for services and the gateway on AWS. Set 1 or more.- The next
infra:deployremoves the wake Lambda, the idle alarms and scale-to-zero policies, and the load balancer's internal listeners. The gateway reaches services over Cloud Map. Until the gateway is rebuilt and redeployed, the old gateway returns errors for every backend route; a fullinfra:deploydoes both. - Custom Lua in
kong.user.ymlthat relied on the wake-up environment variables (WAKEUP_LAMBDA_URLand the Lua sandbox settings) breaks. TheALB_INTERNAL_DNSsecret is no longer used. - Every AWS service keeps at least one task running, so running cost goes up.
Config
accessControl.protectedandaccessControl.cookieTtlHoursininfrastructure.jsonfail validation. Remove them.
For CLI plugin authors
generateKeyAuthPluginandKeyAuthPluginConfigare removed.getDefaultKongPlugins()takes no argument,generateKongDockerfileandwriteKongBuildContextno longer take a provider, andopenApiPathToKongRegexis no longer in the plugin context.ServiceRouteConfigrequirespartnerApi.
nest-common 0.8.0
@tsdevstack/nest-common releases alongside the CLI. New: @Roles(), RolesGuard, ROLES_KEY, @ApiKey(), the AuthType and AuthenticatedApiKey types, RedisService.isReady() and onReady(), and the API key record contract (hashApiKey, buildApiKeyRecordKey, encodeApiKeyRecord and friends). The behavior changes are in the breaking changes above.
Upgrading an existing project
Do these in order. Steps 3 to 7 only apply to projects on the auth template; projects without it skip to the gateway part of step 6.
1. Update the packages and the gateway files.
- In
kong.user.yml, remove every identity header from therequest-transformerremove list. Keep onlyX-Kong-Request-IdandX-Kong-Trust. - If you added Lua plugins to
infrastructure/kong/Dockerfileby hand, move each one into its own folder underkong-plugins/and enable it inkong.user.yml. The Dockerfile is regenerated. - If a service has no
globalPrefixand partners call its old local URLs, setglobalPrefixexplicitly in.tsdevstack/config.json. - If you use a full
kong.custom.yml, add the framework plugins listed in Kong Customization. - Run
npx tsdevstack sync. Then check anything that calls service ports directly, and endpoints that relied on prefix matching, trailing slashes or methods missing from OpenAPI. - Run
npx tsdevstack infra:generate-ciif you use the generated CI workflows.
2. AWS only: drop scale-to-zero. Remove minInstances: 0 from every service and from kong in infrastructure.json (use 1 or more). Run infra:plan to see what goes away, then infra:deploy: it removes the wake machinery, redeploys the gateway, renames the scheduled-job secret so jobs authenticate, and fixes the Next.js assets behind CloudFront. Expect a higher monthly bill. Optionally remove the unused secret with npx tsdevstack cloud-secrets:remove ALB_INTERNAL_DNS --env <env>.
3. Bring the new auth-service code over. Your auth-service is a copy you own, so the framework cannot update it for you. Scaffold a fresh project with the new CLI (npx @tsdevstack/cli init) and copy from its apps/auth-service:
src/api-keys/,src/admin/,src/roles/, and the changes insrc/jobs/,src/user/,src/auth/andsrc/app.module.ts,- the
User,ApiKeyandApiKeyUsagemodels and theSystemRoleandApiKeyStatusenums inprisma/schema.prisma.
Then create the migrations from apps/auth-service, in two steps. First the role rename, written by hand so nobody loses their role:
Replace the generated migration.sql with:
Apply it and create the API key tables:
Regenerate the auth-service client (npx tsdevstack generate-client auth-service) and replace role with systemRole wherever your code or frontend reads it. Commit the migrations; cloud deploys apply them. See Database Migrations.
4. Become admin. Set ADMIN_EMAILS to your email in .secrets.user.json and run npx tsdevstack generate-secrets. In each cloud environment: npx tsdevstack cloud-secrets:set ADMIN_EMAILS --service auth-service --env <env>. Log in with a confirmed account and check that systemRole is ADMIN. Walkthrough: Managing Users.
5. Issue new keys to existing partners. Deploy the auth-service (infra:deploy-service auth-service --env <env>, or a full deploy). Create a key for each partner with POST /auth/v1/admin/api-keys and hand the keys over. The old static keys keep working until the next step, so partners can switch at their own pace.
6. Regenerate and redeploy the gateway. Locally: npx tsdevstack sync. In each cloud environment:
From here only the new keys work. To change the per-IP ceiling on API key routes, set framework.apiKeys.ipLimitPerMinute in .tsdevstack/config.json before this step.
7. Schedule the usage job and clean up. Add the sync-api-key-usage job to scheduledJobs of every environment; infra:generate prints the exact entry for your project, and the API Keys page shows it. Deploy it with npx tsdevstack infra:deploy-schedulers --env <env>. Then delete the partner consumers from kong.user.yml and the old key values from .secrets.user.json and each cloud environment (npx tsdevstack cloud-secrets:remove <KEY> --env <env>).
New projects get all of this from the templates.