Skip to content

About

Convex component for secure API key management — create, validate, revoke, rotate, rate-limit, track usage

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

40 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Convex Component npm version CI License

@vllnt/convex-api-keys

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

Features

  • Secure by default — SHA-256 hashed storage, constant-time comparison, prefix-indexed O(1) lookup, server-side secret generation.
  • Owner checks — key-management methods match the supplied ownerId; the host still authenticates and authorizes callers. configure is a separate host-admin-only surface.
  • Key types — secret and publishable keys with type-encoded prefixes.
  • Finite-use keys — remaining counter 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.

Installation

Requires convex@^1.45.0.

npm install convex @vllnt/convex-api-keys

Prereleases are published under the canary dist-tag (npm install @vllnt/convex-api-keys@canary); pin the resolved version for reproducible installs.

Register 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-limiter at your HTTP action/mutation layer where you have real caller context (IP, auth, plan tier). The component has none. Scope enforcement is yours too: validate returns the key's scopes; check them before acting.

Usage

Expose a host mutation that derives ownership from authenticated identity:

// convex/keys.ts
import { ApiKeys } from "@vllnt/convex-api-keys";
import { components } from "./_generated/api";
import { mutation } from "./_generated/server";
import { v } from "convex/values";

const apiKeys = new ApiKeys(components.apiKeys);

export const create = mutation({
  args: { name: v.string() },
  handler: async (ctx, { name }) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthenticated");
    return apiKeys.create(ctx, { name, ownerId: identity.tokenIdentifier });
  },
});

Show the returned raw key only to its owner; do not log or persist it. For shared organization ownership, replace this mapping with a host authorization check. The following calls illustrate the API inside authorized server handlers:

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.

API Reference

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 Bounded array (default 100; no cursor)
listByTag(ctx, { ownerId, tag, limit? }) query Filter by tag
getUsage(ctx, { keyId, ownerId }) query Usage counter (O(1))

list and listByTag return bounded arrays, not cursor-based pages; repeated calls do not advance through the remaining keys. Authorize reads and mutations in the host before supplying an ownerId.

Full reference: docs/API.md.

Security

  • Protects against accidental cross-tenant bugs in honest host apps — the ownerId check does NOT defend against a compromised host passing a forged ownerId.
  • Derive ownerId from 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.

Testing your app

@vllnt/convex-api-keys/test exports register(t, name = "apiKeys") to mount the component in a convex-test instance. It needs convex-test and vitest (with Vite's import.meta.glob) in the consuming app:

/// <reference types="vite/client" />
import { convexTest } from "convex-test";
import { register } from "@vllnt/convex-api-keys/test";
import shardedCounterTest from "@convex-dev/sharded-counter/test";
import schema from "./schema";

const t = convexTest(schema, import.meta.glob("./**/*.ts"));
register(t, "apiKeys");
shardedCounterTest.register(t, "apiKeys/shardedCounter");

Contributing

See CONTRIBUTING.md for development setup, testing, and PR guidelines.

Author

Built by bntvllnt · bntvllnt.com · X @bntvllnt

Part of the @vllnt Convex component fleet — vllnt.com

If this is useful, sponsor the work.

License

MIT — see LICENSE.

About

Convex component for secure API key management — create, validate, revoke, rotate, rate-limit, track usage

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages