Skip to content

Hashed, revocable API keys per app - #220

Merged
rajat1saxena merged 7 commits into
mainfrom
t3code/port-api-key-management
Oct 19, 2026
Merged

rajat1saxena merged 7 commits into
mainfrom
t3code/port-api-key-management

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Summary

Ports the SendLit/FrontLit API key model to MediaLit. Plan and decisions: docs/prds/apikey.md.

  • Keys are hashed. Only a SHA-256 hash is stored. A key (ml_live_ + 32 random bytes) is shown once, at creation.
  • Apps and keys are separate. api_keys becomes apps (the tenant), with a new api_keys table. An app can have up to 10 active keys, each with a name, prefix, last-used time, optional expiry and soft revocation. Media, settings, signatures and TUS uploads now reference apps.id instead of the secret.
  • Existing integrations keep working. The migration hashes each existing key into a legacy key. A media or settings row that matches no app stops the migration with nothing changed.
  • Revocation actually revokes. Deleted apps' keys no longer authenticate (this was also true of the old TUS path). Revoking a key, or deleting an app, deletes the upload signatures it minted.
  • Header only. Keys are read from x-medialit-apikey; the body apikey field is gone. Uploads now authenticate and check their declared size before the body is read, so an unauthenticated request can't write a large file to disk.
  • Deactivated accounts get 403 account_inactive on every API credential.
  • Bundled installs. MEDIALIT_BOOTSTRAP_API_KEY creates a key for the admin's default app at startup, before the API reports healthy, so SendLit, FrontLit and CourseLit can share one MEDIALIT_APIKEY in their compose files. The admin key is no longer logged.
  • Dashboard. Settings lists an app's keys, creates them in a dialog that shows the secret once, and revokes them after confirmation. The [keyid] route segment is now [appId] (same URL values).

Before merging

  • Upgrade notes (also in self-hosting.mdx): back up the database first. The migration drops the plaintext key column and can't be rolled back. Docker Compose installs migrate automatically; others must run bun apps/api/src/db/migrate.ts.
  • Breaking: a key in the request body is no longer accepted. CourseLit, FrontLit and SendLit already send the header.
  • Follow-up PRs in SendLit, FrontLit and CourseLit to adopt the bundled-compose pattern from docs/prds/apikey.md §6.4.

Test plan

  • API: 145 tests pass, including a migration test that applies 0000–0007, seeds old data, then runs 0008
  • Integration stack (bun run test:integration:stack): REST, TUS, MCP and CLI against real Postgres and MinIO, using a bootstrap-seeded key
  • Web: type-check, build and a headless-browser run of create, reveal-once, revoke and new-app
  • drizzle-kit generate reports no schema drift
  • Try the 0008 migration on a copy of a real database

🤖 Generated with Claude Code

Hashed, revocable, multi-key API keys per app, modelled on SendLit and
FrontLit, plus the operator-supplied bootstrap key for bundled installs.
Ports the SendLit/FrontLit API key model (docs/prds/apikey.md).

- Migration 0008 renames api_keys to apps (the tenant) and adds a new
  api_keys table that stores only SHA-256 hashes. Existing plaintext keys
  become hashed legacy keys, so integrations keep working. Media, settings,
  signatures and TUS uploads now reference apps.id instead of the secret.
- Keys are ml_live_ + 32 random bytes, shown once. Apps can have up to 10
  active keys, with optional expiry and last-used tracking.
- Revoking a key or deleting an app stops its keys and the signatures they
  minted. Deleted apps' keys no longer authenticate, including over TUS.
- API keys are read only from the x-medialit-apikey header; the body field
  is gone. Uploads authenticate and check their declared size before the
  body is read.
- Deactivated accounts get 403 account_inactive on every API credential.
- MEDIALIT_BOOTSTRAP_API_KEY seeds a key for bundled installs at startup;
  the admin key is no longer logged.
- Dashboard API: list, create and revoke keys per app; routes use appId.
The local integration stack sets MEDIALIT_BOOTSTRAP_API_KEY instead of
scraping the admin key from the API log, which no longer contains it.
docker-compose.yml and the env examples document the setting. The SDK's
Media type drops apikey, which the API never returned.
The settings page lists an app's keys by name and prefix with created and
last-used dates, creates keys in a dialog that shows the secret once, and
revokes them after confirmation. The configured bootstrap key shows as
managed by configuration. Creating an app shows its first key once.

The app route segment is now [appId] (same URL values), and the dashboard
no longer receives any key secret after creation.
Adds an API keys page (create, use, rotate, revoke, legacy keys), the
MEDIALIT_BOOTSTRAP_API_KEY setup for bundled Compose installs, and an
upgrade section covering migrations, backups and the hashed-key release.
Creating or renaming an app to a name the user already has now returns 409
with a plain message. It used to return the database error, which included
the failed SQL and internal IDs.

API key dates render with a fixed locale and time zone so server and
browser output match, and the new-app dialog has a description.
Comment thread apps/api/src/apikey/secret.ts Dismissed
@rajat1saxena
rajat1saxena merged commit b95e415 into main Oct 19, 2026
4 of 5 checks passed
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.

3 participants