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

  1. Treat it as success if the existing key is yours: query listEmbedKeys and reuse the already-registered key rather than creating a new one.
  2. Generate a fresh cryptographically random key and retry — a new key yields a new hash and passes the guard.
  3. Debounce/disable the create button while the mutation is in flight, and make client retries idempotent (check for the key before re-submitting).
  4. 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

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


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)