Skip to content

feat(webapp,database): API key rotation grace period#3420

Merged
ericallam merged 2 commits intomainfrom
feat/api-key-rotation-grace-period
Apr 20, 2026
Merged

feat(webapp,database): API key rotation grace period#3420
ericallam merged 2 commits intomainfrom
feat/api-key-rotation-grace-period

Conversation

@ericallam
Copy link
Copy Markdown
Member

Summary

Regenerating a RuntimeEnvironment API key no longer immediately invalidates the previous one. Rotation is now overlap-based: the old key keeps working for 24 hours so customers can roll it out in their env vars without downtime, then stops working.

Design

  • New RevokedApiKey table (one row per revocation). Holds the archived apiKey, a FK to the env, an expiresAt, and a createdAt. Indexed on apiKey (high-cardinality equality — single-row hits) and on runtimeEnvironmentId.
  • regenerateApiKey wraps both writes in a single $transaction: insert a RevokedApiKey with expiresAt = now + 24h, update the env with the new apiKey/pkApiKey.
  • findEnvironmentByApiKey does a two-step lookup: primary unique-index hit on RuntimeEnvironment.apiKey first; on miss, RevokedApiKey.findFirst({ apiKey, expiresAt: { gt: now } }) with an include: { runtimeEnvironment }. Two-step (not OR-join) keeps the hot path identical to today and puts the fallback cost only on invalid keys. Both lookups use $replica.
  • Admin endpoint POST /admin/api/v1/revoked-api-keys/:id accepts { expiresAt } and updates the row. Setting to now ends the grace window immediately; setting to the future extends it.
  • Modal copy on the regenerate dialog updated — previously warned of downtime, now explains the 24h overlap.

Why a separate table instead of columns on RuntimeEnvironment

  • Keeps the hot auth path's primary lookup unchanged — no OR/nullable-apiKey semantics to reason about.
  • Naturally supports multiple in-flight grace windows (regenerate twice in a day → two old keys valid until their independent expiries).
  • FK + cascade cleans up correctly when an env is deleted; nothing to backfill.

Test plan

Verified locally against hello-world with dev and prod env keys:

  • baseline — current key authenticates (GET /api/v1/runs) → 200
  • regenerate via UI — DB shows old key in RevokedApiKey with expiresAt ≈ now+24h, env has new key
  • grace window — both old and new keys → 200; bogus key → 401
  • admin endpoint: expiresAt = now → old key 401
  • admin endpoint: expiresAt = +1h (after early-expire) → old key 200 again
  • admin endpoint: expiresAt = past → old key 401
  • admin 400 (invalid body), 404 (unknown id), 401 (missing/non-admin PAT)
  • same flow exercised end-to-end on a PROD-typed env — behavior identical
  • pnpm run typecheck --filter webapp passes

Regenerating a RuntimeEnvironment API key no longer immediately invalidates the
previous key. The old key is archived in a new `RevokedApiKey` table with a
24-hour expiresAt. `findEnvironmentByApiKey` falls back to this table when the
primary lookup misses. An admin endpoint
(`POST /admin/api/v1/revoked-api-keys/:id`) lets us shorten or extend the
grace window by updating `expiresAt`.

- schema: new RevokedApiKey model, indexes on apiKey and runtimeEnvironmentId
- regenerateApiKey: wraps archive + update in a single transaction
- modal copy updated to describe the 24h overlap instead of downtime
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 20, 2026

⚠️ No Changeset found

Latest commit: 2a63995

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e63bf722-1e8d-4072-b09b-563e5f509960

📥 Commits

Reviewing files that changed from the base of the PR and between d21217d and 2a63995.

📒 Files selected for processing (1)
  • apps/webapp/app/routes/api.v1.auth.jwt.ts
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (28)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

Files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

Add crumbs as you write code using // @Crumbs comments or `// `#region` `@crumbs blocks. These are temporary debug instrumentation and must be stripped using agentcrumbs strip before merge.

Files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
**/*.ts{,x}

📄 CodeRabbit inference engine (CLAUDE.md)

Always import from @trigger.dev/sdk when writing Trigger.dev tasks. Never use @trigger.dev/sdk/v3 or deprecated client.defineJob.

Files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: Access environment variables through the env export of env.server.ts instead of directly accessing process.env
Use subpath exports from @trigger.dev/core package instead of importing from the root @trigger.dev/core path

Use named constants for sentinel/placeholder values (e.g. const UNSET_VALUE = '__unset__') instead of raw string literals scattered across comparisons

Files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
🧠 Learnings (2)
📚 Learning: 2026-03-22T13:26:12.060Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/code/TextEditor.tsx:81-86
Timestamp: 2026-03-22T13:26:12.060Z
Learning: In the triggerdotdev/trigger.dev codebase, do not flag `navigator.clipboard.writeText(...)` calls for `missing-await`/`unhandled-promise` issues. These clipboard writes are intentionally invoked without `await` and without `catch` handlers across the project; keep that behavior consistent when reviewing TypeScript/TSX files (e.g., usages like in `apps/webapp/app/components/code/TextEditor.tsx`).

Applied to files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
📚 Learning: 2026-03-22T19:24:14.403Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3187
File: apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts:200-204
Timestamp: 2026-03-22T19:24:14.403Z
Learning: In the triggerdotdev/trigger.dev codebase, webhook URLs are not expected to contain embedded credentials/secrets (e.g., fields like `ProjectAlertWebhookProperties` should only hold credential-free webhook endpoints). During code review, if you see logging or inclusion of raw webhook URLs in error messages, do not automatically treat it as a credential-leak/secrets-in-logs issue by default—first verify the URL does not contain embedded credentials (for example, no username/password in the URL, no obvious secret/token query params or fragments). If the URL is credential-free per this project’s conventions, allow the logging.

Applied to files:

  • apps/webapp/app/routes/api.v1.auth.jwt.ts
🔇 Additional comments (1)
apps/webapp/app/routes/api.v1.auth.jwt.ts (1)

39-43: LGTM — signing now uses the canonical environment key.

This correctly keeps JWTs minted during the revoked-key grace window valid against the environment’s current signing secret.


Walkthrough

Regenerating a RuntimeEnvironment API key now records the previous key in a new RevokedApiKey table with a 24-hour expiresAt, instead of immediately invalidating it. findEnvironmentByApiKey first looks up live environments and, if none match, falls back to non-expired RevokedApiKey entries to return the associated environment. A new admin route allows updating a revoked key’s expiresAt. Prisma schema and migration add the RevokedApiKey model and relation. API key rotation is performed inside a transaction. UI copy for the regenerate-key modal and JWT signing now use the environment apiKey where applicable.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: introducing a 24-hour grace period for API key rotation in the webapp and database layers.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, design rationale, table schema, implementation details, test plan, and reasoning for architectural decisions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-key-rotation-grace-period

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ericallam ericallam marked this pull request as ready for review April 20, 2026 16:43
coderabbitai[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

During the grace window after an API key rotation, a client calling
/api/v1/auth/jwt with their old (revoked but still valid) key would get
back a JWT that immediately failed validation, because signing used the
header key (old) while validation in jwtAuth.server.ts uses
environment.apiKey (new). Sign with the environment's canonical current
key instead so minted JWTs validate regardless of which accepted key was
used to authenticate the mint call. Works for dev/prod/preview — in the
PREVIEW branch path, findEnvironmentByApiKey already merges the parent's
current apiKey onto the returned env.
@ericallam ericallam merged commit 03e4d5f into main Apr 20, 2026
41 checks passed
@ericallam ericallam deleted the feat/api-key-rotation-grace-period branch April 20, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants