Secure API key management as a Convex component — create, validate, revoke, rotate, and track usage with built-in auth boundaries and structured audit logging.
const apiKeys = new ApiKeys(components.apiKeys, { prefix: "myapp" });
const { key, keyId } = await apiKeys.create(ctx, { name: "SDK Key", ownerId: orgId });
const result = await apiKeys.validate(ctx, { key: bearerToken });
// result.valid → { keyId, ownerId, scopes, ... } | result.reason- Secure by default — SHA-256 hashed storage, constant-time comparison, prefix-indexed O(1) lookup, server-side secret generation.
- Auth boundary —
ownerIdrequired on all admin mutations; prevents cross-tenant access. - Key types —
secretandpublishablekeys with type-encoded prefixes. - Finite-use keys —
remainingcounter with atomic decrement for one-time-use tokens. - Disable / enable — reversible pause without revoking.
- Rotation — configurable grace period (60s–30d) where both old and new keys are valid.
- Bulk revoke by tag, tags & environments, multi-tenant scoping, usage tracking, input validation, structured audit logging.
Peer dependency: convex@^1.36.1
npm install convex@^1.36.1 @vllnt/convex-api-keysRegister in your convex/convex.config.ts:
import { defineApp } from "convex/server";
import apiKeys from "@vllnt/convex-api-keys/convex.config";
const app = defineApp();
app.use(apiKeys);
export default app;Rate limiting is your responsibility — add
@convex-dev/rate-limiterat your HTTP action/mutation layer where you have real caller context (IP, auth, plan tier). The component has none.
import { ApiKeys } from "@vllnt/convex-api-keys";
import { components } from "./_generated/api";
const apiKeys = new ApiKeys(components.apiKeys, {
prefix: "myapp", // key prefix (default: "vk")
defaultType: "secret",
});
// Create — secret material is generated server-side, returned once.
const { key, keyId } = await apiKeys.create(ctx, {
name: "Production SDK Key",
ownerId: orgId,
type: "secret",
scopes: ["read:users", "write:orders"],
tags: ["sdk", "v2"],
env: "live",
});
// key = "myapp_secret_live_a1b2c3d4_<64-char-hex>"
// Validate — track usage and narrow on `valid`.
const result = await apiKeys.validate(ctx, { key: bearerToken });
if (!result.valid) {
// result.reason: "malformed" | "not_found" | "revoked" | "expired" | "exhausted" | "disabled"
return new Response("Unauthorized", { status: 401 });
}
const { ownerId, scopes, tags, env, type, metadata, remaining } = result;See docs/API.md for the full method reference, key format, and lifecycle.
| Method | Ctx | Description |
|---|---|---|
create(ctx, options) |
mutation | Create a new API key |
validate(ctx, { key }) |
mutation | Validate and track usage |
revoke(ctx, { keyId, ownerId }) |
mutation | Permanently revoke a key |
revokeByTag(ctx, { ownerId, tag }) |
mutation | Bulk revoke by tag |
rotate(ctx, { keyId, ownerId, gracePeriodMs? }) |
mutation | Rotate with grace period |
update(ctx, { keyId, ownerId, name?, ... }) |
mutation | Update metadata in-place |
disable(ctx, { keyId, ownerId }) |
mutation | Temporarily disable |
enable(ctx, { keyId, ownerId }) |
mutation | Re-enable a disabled key |
configure(ctx, { ... }) |
mutation | Runtime config (admin-only) |
list(ctx, { ownerId, env?, status?, limit? }) |
query | List keys (paginated, default 100) |
listByTag(ctx, { ownerId, tag, limit? }) |
query | Filter by tag |
getUsage(ctx, { keyId, ownerId }) |
query | Usage counter (O(1)) |
Full reference: docs/API.md.
- Protects against accidental cross-tenant bugs in honest host apps — the
ownerIdcheck does NOT defend against a compromised host passing a forgedownerId. - Derive
ownerIdfrom your own auth layer (e.g.ctx.auth.getUserIdentity()) before passing it in. - Raw keys are never stored — only the SHA-256 hash; the raw value is returned once at creation.
See docs/API.md.
import { convexTest } from "convex-test";
import { register } from "@vllnt/convex-api-keys/test";
import shardedCounterTest from "@convex-dev/sharded-counter/test";
const t = convexTest(schema, modules);
register(t, "apiKeys");
shardedCounterTest.register(t, "apiKeys/shardedCounter");See CONTRIBUTING.md for development setup, testing, and PR guidelines.
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
MIT — see LICENSE.