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

  1. Call revokeEmbedKey on unused active keys first (releases quota since only non-revoked keys count), then create the new key.
  2. Reuse an existing active key instead of minting a new one — list them with listEmbedKeys and pick an unrevoked one.
  3. 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.
  4. 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

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


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)