zenkey
v0.0.1
Published
Type-safe, dead simple API key and rate limit management
Maintainers
Readme
Zenkey
Type-safe, adapter-driven API key verification with owner-wide and key-specific usage limits.
Quickstart
import Database from "better-sqlite3";
import { SqliteContentAdapter, Zenkey } from "zenkey";
type AppSchema = {
ownerMeta: { stripeCustomerId: string; plan: "free" | "pro" };
keyMeta: { environment: "dev" | "prod"; projectId: string };
verifyContext: { requestId: string; route: string };
};
const db = new Database("zenkey.db");
const adapter = new SqliteContentAdapter<AppSchema>(db, {
names: {
owners: "workspace_users",
keys: "workspace_api_keys",
limits: "workspace_limits",
},
});
await adapter.migrate();
const zenkey = new Zenkey<AppSchema>({ adapter });
await zenkey.owners.create({
ownerId: "user_123",
ownerMeta: { stripeCustomerId: "cus_123", plan: "pro" },
});
const { key, keyId } = await zenkey.keys.create({
ownerId: "user_123",
name: "prod",
keyMeta: { environment: "prod", projectId: "proj_123" },
usage: [
{ name: "weekly", limit: 10_000, window: "1w" },
{ name: "trial", limit: 500, window: "5min" },
],
});
const verdict = await zenkey.keys.verify({
key,
usage: 1,
context: { requestId: "req_123", route: "/v1/usage" },
});
if (!verdict.valid) {
throw new Error(verdict.code);
}
await zenkey.keys.usage.add({ keyId, name: "weekly", amount: 50 });Included adapters
MemoryContentAdapterSqliteContentAdapterPostgresContentAdapterMongoContentAdapter
Storage design
Zenkey keeps the v1 model to three logical records only:
ownerskeyslimits
The hot paths stay narrow on purpose:
owners.idis the publicownerId, so owner lookups hit the primary key directly.keys.secret_hashis the only secret lookup path and is unique.keys.owner_idis indexed for list-by-owner queries.limits.idis deterministic (owner:<ownerId>:<name>orkey:<keyId>:<name>) so updates and deletes avoid extra unique indexes.limitsuses a narrow owner-scope index and a narrow key-scope index instead of wider composite indexes.
Two extra limit fields are stored in v1:
window_unitwindow_timezone
Those fields keep future calendar-aware reset behavior possible without changing the public API or rewriting every existing limit row.
Migrations and indexes
SQLite:
const statements = SqliteContentAdapter.getSchemaStatements();Postgres:
const statements = PostgresContentAdapter.getSchemaStatements();Mongo:
const indexDefinitions = MongoContentAdapter.getIndexDefinitions();Notes on other databases
Zenkey ships SQLite, Postgres, Mongo, and Memory in this quickstart release.
For future adapters, keep the same cheap lookup pattern:
- MySQL can mirror the Postgres schema closely: owner PK, unique
secret_hash, indexedowner_id, and split limit-scope indexes. - DynamoDB should prefer primary-key reads over GSIs for the hot scope records:
- owner meta:
PK = OWNER#<ownerId>,SK = META - owner limit:
PK = OWNER#<ownerId>,SK = LIMIT#<name> - key meta:
PK = KEY#<keyId>,SK = META - key limit:
PK = KEY#<keyId>,SK = LIMIT#<name> - secret lookup can use one narrow GSI keyed by
SECRET#<secretHash>
- owner meta:
