koala73/worldmonitor · error · ConvexError
DUPLICATE_KEY
DUPLICATE_KEY
Error message
DUPLICATE_KEY
What it means
createEmbedKey looks up an existing embedKeys row by keyHash on the by_keyHash index and throws "DUPLICATE_KEY" if one is found. Since only the SHA-256 hash of the key is stored, two rows with the same hash would be indistinguishable at validation time; this belt-and-suspenders guard rejects a second registration of the same key material. (Collisions are astronomically unlikely, so this usually means the identical key was submitted twice.)
Solutions
- Treat it as success if the existing key is yours: query listEmbedKeys and reuse the already-registered key rather than creating a new one.
- Generate a fresh cryptographically random key and retry — a new key yields a new hash and passes the guard.
- Debounce/disable the create button while the mutation is in flight, and make client retries idempotent (check for the key before re-submitting).
- If the key was revoked earlier, note that the duplicate check queries by hash regardless of revocation — you must generate a new key, not re-register the old hash.
Example fix
// before
await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash }); // DUPLICATE_KEY on retry
// after
try {
await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });
} catch (e) {
if (!String(e).includes('DUPLICATE_KEY')) throw e; // idempotent retry: key already exists
} Defensive patterns
Strategy: try-catch
Validate before calling
const keys = await client.query(api.embedKeys.listEmbedKeys, {});
if (keys.some(k => k.revokedAt === null)) {
// reuse an existing active key instead of minting a new one
return keys.find(k => k.revokedAt === null);
} Try / catch
try {
return await client.mutation(api.embedKeys.createEmbedKey, { name, keyPrefix, keyHash });
} catch (e) {
if (String(e).includes('DUPLICATE_KEY')) {
return await client.query(api.embedKeys.listEmbedKeys, {}).then(ks => ks.find(k => k.revokedAt === null));
} else throw e;
} Prevention
- Disable the create button while the mutation is in flight (single in-flight request).
- Make create flows idempotent: reuse existing active keys before generating new ones.
- Never use fixed/deterministic keys in seeds or scripts — always generate fresh randomness.
- Remember the duplicate check ignores revocation: a revoked key's hash still blocks re-registration.
When it happens
Trigger: Calling createEmbedKey twice with the same generated key (double-click on the create button, optimistic-UI retry after a slow response, or re-running a migration/script that mints keys); the same key was already registered under this or another user.
Common situations: UI not disabling the submit button during the in-flight mutation; a backend job replayed after a timeout; a developer re-running a seed script with a deterministic/fixed key; or copying the same keyHash fixture into multiple test calls.
Understand the failure class
Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.
Related errors
- INVALID_API_KEY_SCOPES
- CUSTOMER_REFERENCE_CONFLICT
- Webhook URL must not point to a private/local address
- HTTP ${response.status}
- REDIS_DOWN
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/4e2166610edfecdc.
Report an issue: GitHub.
Appendix: source
Thrown at convex/embedKeys.ts:112
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(),
});
return { id, name: args.name.trim(), keyPrefix: args.keyPrefix, allowedOrigins };
},
});
/** List all embed keys for the current user (active + revoked). */
export const listEmbedKeys = query({
args: {},View on GitHub (pinned to 7d06c8633d)