koala73/worldmonitor · error · ConvexError
KEY_LIMIT_REACHED
KEY_LIMIT_REACHED
Error message
KEY_LIMIT_REACHED
What it means
createEmbedKey counts the caller's active (non-revoked) embed keys via the by_userId_revokedAt index and throws "KEY_LIMIT_REACHED" when there are already MAX_EMBED_KEYS_PER_USER (5) of them. This is a quota guard: embed keys are published in partner HTML, so the server caps how many live keys each account can hold. The mutation is rejected before the duplicate-hash check and insert.
Solutions
- Call revokeEmbedKey on unused active keys first (releases quota since only non-revoked keys count), then create the new key.
- Reuse an existing active key instead of minting a new one — list them with listEmbedKeys and pick an unrevoked one.
- If the limit legitimately blocks a real use case, request a higher MAX_EMBED_KEYS_PER_USER or split usage across accounts per product policy.
- Add client-side guard: fetch listEmbedKeys, count keys with revokedAt === null, and prompt cleanup before allowing creation.
Example fix
// before
await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash }); // KEY_LIMIT_REACHED
// after
const keys = await client.query(api.embedKeys.listEmbedKeys, {});
for (const k of keys.filter(k => k.revokedAt === null).slice(0, keys.length - 4)) {
await client.mutation(api.embedKeys.revokeEmbedKey, { keyId: k.id });
}
await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash }); Defensive patterns
Strategy: validation
Validate before calling
const MAX_EMBED_KEYS_PER_USER = 5;
async function canCreateEmbedKey(client) {
const keys = await client.query(api.embedKeys.listEmbedKeys, {});
return keys.filter(k => k.revokedAt === null).length < MAX_EMBED_KEYS_PER_USER;
} Try / catch
try {
await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });
} catch (e) {
if (String(e).includes('KEY_LIMIT_REACHED')) {
openKeyCleanupDialog(); // prompt user to revoke unused keys
} else throw e;
} Prevention
- Check the active-key count with listEmbedKeys before minting a new key.
- Revoke keys as part of offboarding sites/environments, not just creating them.
- Never share one account across many integrations that each mint keys.
- In tests, revoke keys in afterEach to avoid quota buildup.
When it happens
Trigger: Calling createEmbedKey when the authenticated user already has 5 embedKeys rows with revokedAt === undefined (undefined means 'not revoked' in the index query). Revoked keys do not count, so this only fires on the 6th concurrent active key.
Common situations: Partner integrations spinning up a new embed key per site or environment without revoking old ones; retry storms creating duplicates after perceived failures; test suites minting keys repeatedly under one shared account; or a team account that pooled usage into a single user and exhausted its 5-key allowance.
Related errors
- INVALID_API_KEY_SCOPES
- Firecrawl extract error: ${data.error ?? 'unknown'}
- COMPANY_LIMIT_REACHED
- Webhook URL must not point to a private/local address
- HTTP ${response.status}
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/612612be45880e1f.
Report an issue: GitHub.
Appendix: source
Thrown at convex/embedKeys.ts:103
if (!args.name.trim()) {
throw new ConvexError("INVALID_NAME");
}
if (!/^wme_[a-f0-9]{5}$/.test(args.keyPrefix)) {
throw new ConvexError("INVALID_PREFIX");
}
if (!/^[a-f0-9]{64}$/.test(args.keyHash)) {
throw new ConvexError("INVALID_HASH");
}
const allowedOrigins = normalizeAllowedOrigins(args.allowedOrigins);
const active = await ctx.db
.query("embedKeys")
.withIndex("by_userId_revokedAt", (q) =>
q.eq("userId", userId).eq("revokedAt", undefined),
)
.collect();
if (active.length >= MAX_EMBED_KEYS_PER_USER) {
throw new ConvexError("KEY_LIMIT_REACHED");
}
// Guard against duplicate hash (astronomically unlikely, but belt-and-suspenders)
const dup = await ctx.db
.query("embedKeys")
.withIndex("by_keyHash", (q) => q.eq("keyHash", args.keyHash))
.first();
if (dup) {
throw new ConvexError("DUPLICATE_KEY");
}
const id = await ctx.db.insert("embedKeys", {
userId,
name: args.name.trim(),
keyPrefix: args.keyPrefix,
keyHash: args.keyHash,
allowedOrigins,
createdAt: Date.now(),View on GitHub (pinned to 7d06c8633d)