{"record":{"id":"8e0c6e52f0a17d92","repo":"koala73/worldmonitor","slug":"webhookenvelope-required-for-slack-channel","errorCode":null,"errorMessage":"webhookEnvelope required for slack channel","messagePattern":"webhookEnvelope required for slack channel","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/notificationChannels.ts","lineNumber":501,"sourceCode":"    const existing = await ctx.db\n      .query(\"notificationChannels\")\n      .withIndex(\"by_user_channel\", (q) =>\n        q.eq(\"userId\", userId).eq(\"channelType\", args.channelType),\n      )\n      .unique();\n\n    const now = Date.now();\n\n    if (args.channelType === \"telegram\") {\n      if (!args.chatId) throw new ConvexError(\"chatId required for telegram channel\");\n      const doc = { userId, channelType: \"telegram\" as const, chatId: args.chatId, verified: true, linkedAt: now };\n      if (existing) {\n        await ctx.db.replace(existing._id, doc);\n      } else {\n        await ctx.db.insert(\"notificationChannels\", doc);\n      }\n    } else if (args.channelType === \"slack\") {\n      if (!args.webhookEnvelope) throw new ConvexError(\"webhookEnvelope required for slack channel\");\n      const doc = { userId, channelType: \"slack\" as const, webhookEnvelope: args.webhookEnvelope, verified: true, linkedAt: now };\n      if (existing) {\n        await ctx.db.replace(existing._id, doc);\n      } else {\n        await ctx.db.insert(\"notificationChannels\", doc);\n      }\n    } else if (args.channelType === \"email\") {\n      if (!args.email) throw new ConvexError(\"email required for email channel\");\n      const doc = { userId, channelType: \"email\" as const, email: args.email, verified: true, linkedAt: now };\n      if (existing) {\n        await ctx.db.replace(existing._id, doc);\n      } else {\n        await ctx.db.insert(\"notificationChannels\", doc);\n      }\n    } else if (args.channelType === \"webhook\") {\n      if (!args.webhookEnvelope) throw new ConvexError(\"webhookEnvelope required for webhook channel\");\n      const doc = { userId, channelType: \"webhook\" as const, webhookEnvelope: args.webhookEnvelope, verified: true, linkedAt: now, webhookLabel: args.webhookLabel };\n      if (existing) {","sourceCodeStart":483,"sourceCodeEnd":519,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/notificationChannels.ts#L483-L519","documentation":"Thrown by `setChannel` when `channelType === \"slack\"` but `args.webhookEnvelope` is missing/empty. The slack branch stores a webhook envelope (the incoming-webhook URL/payload from Slack's OAuth install). Plain-string ConvexError; `err.data === \"webhookEnvelope required for slack channel\"`. Note the same guard exists for the `webhook` channel type at line 517 (`webhookEnvelope required for webhook channel`).","triggerScenarios":"Calling `setChannel({ channelType: \"slack\" })` with no `webhookEnvelope`; the Slack OAuth install completed but the incoming-webhook response wasn't captured; the envelope is an empty string.","commonSituations":"The Slack OAuth redirect was handled but the webhook response payload wasn't serialized into the envelope; a partial install (user cancelled the webhook creation step); a test calling the mutation with only the channel type.","solutions":["Complete the Slack OAuth incoming-webhook install and capture the full webhook response as `webhookEnvelope` before calling `setChannel`.","Client-side: disable save until `webhookEnvelope` is present for the slack branch.","On `err.data === \"webhookEnvelope required for slack channel\"`, re-run the Slack install flow."],"exampleFix":"// before\nawait convex.mutation(api.notificationChannels.setChannel, { channelType: \"slack\" });\n\n// after — capture the incoming webhook envelope from Slack OAuth\nconst envelope = JSON.stringify(slackWebhookResponse);\nif (channelType === \"slack\" && !envelope) {\n  setFieldError(\"webhook\", \"Reconnect Slack to generate a webhook.\");\n  return;\n}\nawait convex.mutation(api.notificationChannels.setChannel, { channelType: \"slack\", webhookEnvelope: envelope });","handlingStrategy":"validation","validationCode":"if (channelType === \"slack\" && !webhookEnvelope) {\n  setFieldError(\"webhook\", \"Reconnect Slack to generate a webhook.\");\n  return;\n}","typeGuard":"function hasSlackEnvelope(args: { channelType: string; webhookEnvelope?: string }): boolean {\n  return args.channelType !== \"slack\" || (typeof args.webhookEnvelope === \"string\" && args.webhookEnvelope.length > 0);\n}","tryCatchPattern":"try {\n  await convex.mutation(api.notificationChannels.setChannel, { channelType: \"slack\", webhookEnvelope: envelope });\n} catch (err) {\n  if (err.data === \"webhookEnvelope required for slack channel\") setFieldError(\"webhook\", \"Reconnect Slack.\");\n  else throw err;\n}","preventionTips":["Complete the Slack OAuth incoming-webhook install and capture the full response as webhookEnvelope.","Disable save until webhookEnvelope is present for the slack branch.","Re-run the Slack install flow if the envelope is missing.","The same guard exists for the generic \"webhook\" channel type at line 517."],"tags":["validation","convex","notifications","slack"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}