koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_ACCESS_DENIED

Error message

COMPANY_MONITORING_ACCESS_DENIED

What it means

Thrown by requireProvisionedAccount (accounts.ts:350-353) after ensureActiveAccount fails to find or provision an active account for the ownerUserId. Unlike requireActiveAccount, this path first attempts to provision (sync from entitlement) and only throws if provisioning also yields no active account. This is the entry-point guard for mutations that are allowed to create the account on first use.

Source

Thrown at convex/companyMonitoring/accounts.ts:352

 * Provisioning delegates to the same state machine the reaper uses, so a
 * terminal tombstone still refuses to yield an active account: the sync returns
 * the terminal row untouched and the re-resolve below rejects it.
 */
export async function ensureActiveAccount(
  ctx: MutationCtx,
  ownerUserId: string,
  knownEntitlement?: Doc<"entitlements"> | null,
) {
  const existing = await activeAccountForOwner(ctx, ownerUserId, knownEntitlement);
  if (existing) return existing;
  await syncCompanyMonitoringAccountFromEntitlement(ctx, ownerUserId);
  return activeAccountForOwner(ctx, ownerUserId, knownEntitlement);
}

/** `requireActiveAccount` for the entry points that may provision. */
export async function requireProvisionedAccount(ctx: MutationCtx, ownerUserId: string) {
  const account = await ensureActiveAccount(ctx, ownerUserId);
  if (!account) throw new ConvexError("COMPANY_MONITORING_ACCESS_DENIED");
  return account;
}

async function terminalize(
  ctx: MutationCtx,
  ownerUserId: string,
  terminalReason: "owner_deleted" | "account_deleted",
  existing?: Doc<"companyMonitoringAccounts"> | null,
) {
  const ownerFence = await companyMonitoringOwnerFenceCandidates(ownerUserId);
  const ownerFenceHash = ownerFence.current;
  const match = await findAccountByOwnerFence(ctx, ownerFence);
  if (existing && match && existing._id !== match.account._id) {
    throw new ConvexError("ACCOUNT_OWNER_FENCE_CONFLICT");
  }
  const account = existing ?? match?.account ?? null;
  const now = Date.now();
  if (!account) {

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Confirm the user has an active paid entitlement (planKey !== 'free', features.tier > 0, validUntil >= now) before invoking the mutation.
  2. If the user is anonymous (browser UUID), they must complete claimSubscription to bind a real owner before Company Monitoring is available.
  3. If the entitlement exists but provisioning fails, inspect the entitlements row and canonicalEntitlement output for the userId.
  4. Gate the client UI on entitlement status so the mutation is never called for ineligible users.
Defensive patterns

Strategy: validation

Validate before calling

// Before calling a provisioning-eligible Company Monitoring endpoint:
const entitled = entitlement && entitlement.planKey !== "free" && entitlement.features.tier > 0 && entitlement.validUntil >= Date.now();
if (!entitled) {
  // show paywall; do not call the backend
}

Try / catch

try {
  await ctx.runMutation(internal.companyMonitoring.someProvisioningEndpoint, args);
} catch (err) {
  if (err instanceof ConvexError && err.message === "COMPANY_MONITORING_ACCESS_DENIED") {
    // user not entitled or anonymous — surface paywall/claim flow
    throw new Error("Company Monitoring requires an active PRO subscription.");
  }
  throw err;
}

Prevention

When it happens

Trigger: The user has no active entitlement (free plan, expired, features.tier === 0). The user's entitlement validUntil is in the past. The entitlement row exists but the account cannot be provisioned because canonical.active is false. The userId is an anonymous browser UUID (ANON_ID_V4_REGEX) which is deliberately skipped.

Common situations: A free-tier user invokes a provisioning-eligible Company Monitoring mutation. A user's subscription expired between the UI check and the backend call. The entitlement sync has not yet propagated. An anonymous (pre-claim) user attempts the action.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/01e9ac4823d87d8a. Report an issue: GitHub.