{"product":"Alert","version":"2025-09","origin":"https://localhost:3000","summary":"Alert adds a hosted, live notification bell to your app. The human provides only three values (Alert URL, server key, app URL). You, the AI agent, do everything else: verify the key, read the app's real layout, place the bell, create the token route, set env, build, and confirm it is live.","humanInputs":[{"key":"ALERT_URL","description":"Alert origin, e.g. https://alertqc.compucom.com (QC) or https://alert.compucom.com (prod). Locally http://localhost:3002.","example":"https://localhost:3000"},{"key":"ALERT_SERVER_KEY","description":"Alert-managed server key from Company → Connect an App. Server-only; never exposed to the browser.","example":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"},{"key":"APP_URL","description":"The URL the integrating app runs on (must match the domain registered for the key), e.g. https://scottyqc.compucom.com or http://localhost:3000.","example":"http://localhost:3000"}],"rules":["NEVER put the API key in the browser. It goes in server env as ALERT_SERVER_KEY (no NEXT_PUBLIC_ prefix).","NEVER hardcode a user id. User identity comes from the app's own session via the token route.","Read the app's actual layout/header file and place the bell where it belongs — do not guess with regex.","Only create/modify the minimum: the token route, the env vars, and one bell <script> line in the layout.","Be idempotent: if the token route or bell already exist, do not duplicate them.","After changes, run the app's build and fix any errors you introduced before declaring success.","DETECT the stack before writing anything: read package.json (and lockfiles / requirements.txt / pyproject.toml / go.mod / Gemfile) to identify the framework, then pick the matching template from tokenRouteTemplates. Do not assume Next.js.","MONOREPO / multi-app repos: the APP_URL tells you which app to integrate. Find the package/app that actually serves that domain (check each app's config, ports, or deploy manifest), and make ALL changes inside THAT app's directory — its own layout, its own /api, its own .env. Do not touch sibling apps.","READ THE JSON, NOT THE HTTP STATUS. verify, diagnose, and check-token return HTTP 200 even when the result is a failure — the verdict is in the body (valid/ready/healthy fields). Never treat a 200 as success without reading the JSON.","ROLLBACK ON FAILURE: if your changes break the build and you cannot fix it within 2 attempts, revert the files you changed and report exactly what failed. Never leave the app in a broken state.","DO NOT CLAIM SUCCESS until every item in successContract is satisfied (see below). 'The bell connected' is NOT success — a real test notification must be received."],"successContract":[{"id":"verified","check":"POST /api/embed/verify returns valid:true AND ready:true (domain matches).","proves":"The key is real and the app's domain is correctly registered."},{"id":"token_route","check":"Call the app's own /api/alert-token with a logged-in session to get a token, then POST it to /api/embed/check-token — it must return valid:true with the correct workspace.","proves":"The token route is wired to the right key + session + workspace. THIS is the step most integrations get silently wrong."},{"id":"bell_live","check":"After loading a page of the running app while logged in, POST /api/embed/verify again — alreadyLive must be true.","proves":"The bell <script> is actually on the page and loading."},{"id":"notification_received","check":"POST /api/embed/test-notification for the logged-in user, then confirm that exact notificationId appears (in the bell, or via GET /api/notifications).","proves":"The full path works end to end — not just that things connected."}],"steps":[{"n":1,"title":"Collect the three inputs","detail":"Ask the human for ALERT_URL, ALERT_SERVER_KEY, and APP_URL if not already provided. Nothing else is needed from them."},{"n":2,"title":"Verify with Alert","detail":"POST {key, appUrl} to ALERT_URL/api/embed/verify. Read the JSON: if valid=false, ask for a correct key. If domainMatches=false, tell the human the app URL doesn't match the registered domain and follow nextSteps. Proceed only when valid=true and the domain is registered/matching.","request":{"method":"POST","url":"https://localhost:3000/api/embed/verify","body":{"key":"<ALERT_SERVER_KEY>","appUrl":"<APP_URL>"}}},{"n":3,"title":"Detect the app's stack (and the right sub-app in a monorepo)","detail":"Read package.json / lockfiles / requirements.txt / pyproject.toml / go.mod / Gemfile to identify the framework (Next.js App or Pages Router, Express, Fastify, Django, Flask, other). In a MONOREPO or multi-app repo, use APP_URL to find which package actually serves that domain (check each app's config/ports/deploy manifest) and confine ALL changes to that app's directory. Then pick the matching template from tokenRouteTemplates below — do not improvise the HTTP call."},{"n":4,"title":"Set environment variables","detail":"Add these to the app's .env (create if missing). The key must stay server-only.","env":{"NEXT_PUBLIC_ALERTS_URL":"<ALERT_URL>","ALERT_SERVER_KEY":"<ALERT_SERVER_KEY>"}},{"n":5,"title":"Create the token route (identity)","detail":"Create app/api/alert-token/route.ts (Next.js App Router; adjust path/pattern for the app's framework and router). Adapt the session import to the app's real auth (e.g. next-auth `auth()`, `getServerSession`, Clerk, custom). It must return { token, expiresIn } from Alert's session-token endpoint.","framework":"next-app-router","file":"app/api/alert-token/route.ts","source":"import { NextResponse } from \"next/server\";\nimport { auth } from \"@/auth\"; // your existing SSO/session helper\n\nexport async function GET() {\n  const session = await auth();\n  const email = session?.user?.email;\n  if (!email) return NextResponse.json({ error: \"not_authenticated\" }, { status: 401 });\n\n  // Read from env per environment (QC vs prod) — no hardcoded fallback.\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL ?? \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY ?? \"\";\n  if (!alertUrl || !serverKey) return NextResponse.json({ error: \"alert_not_configured\" }, { status: 500 });\n\n  try {\n    const res = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n      cache: \"no-store\",\n    });\n    if (!res.ok) return NextResponse.json({ error: \"token_mint_failed\" }, { status: 502 });\n    return NextResponse.json(await res.json());\n  } catch {\n    return NextResponse.json({ error: \"alert_unreachable\" }, { status: 502 });\n  }\n}","adaptNotes":["Pick the template matching the app's stack from `tokenRouteTemplates` below — Next.js App/Pages Router, Express, Fastify, Django, Flask, or generic.","Replace the session/user lookup line with however the app identifies the logged-in user server-side.","Whatever the stack, the route must live at /api/alert-token (same origin as the app) and return { token, expiresIn }.","CRITICAL — auth middleware: if the app has global auth middleware/guard that redirects unauthenticated requests to a login PAGE (Next.js middleware, next-auth `auth()` middleware, proxy.ts, an Express auth middleware, etc.), you MUST exclude /api/alert-token from that redirect. Otherwise the bell's request to /api/alert-token is redirected to the login HTML page, the bell tries to JSON.parse HTML, and it fails with \"Unexpected token '<'\" and stays Offline. For Next.js middleware, add `api/alert-token` to the matcher's negative-lookahead exclusions (next to api/auth). The route already checks the session itself and returns proper JSON (200 or 401)."],"tokenRouteTemplates":[{"framework":"Next.js (App Router)","file":"app/api/alert-token/route.ts","language":"ts","sessionNote":"Replace `auth()` with the app's real session helper (next-auth v5 `auth()`, `getServerSession(authOptions)`, Clerk `auth()`, etc.).","source":"import { NextResponse } from \"next/server\";\nimport { auth } from \"@/auth\"; // your existing SSO/session helper\n\nexport async function GET() {\n  const session = await auth();\n  const email = session?.user?.email;\n  if (!email) return NextResponse.json({ error: \"not_authenticated\" }, { status: 401 });\n\n  // Read from env per environment (QC vs prod) — no hardcoded fallback.\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL ?? \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY ?? \"\";\n  if (!alertUrl || !serverKey) return NextResponse.json({ error: \"alert_not_configured\" }, { status: 500 });\n\n  try {\n    const res = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n      cache: \"no-store\",\n    });\n    if (!res.ok) return NextResponse.json({ error: \"token_mint_failed\" }, { status: 502 });\n    return NextResponse.json(await res.json());\n  } catch {\n    return NextResponse.json({ error: \"alert_unreachable\" }, { status: 502 });\n  }\n}"},{"framework":"Next.js (Pages Router)","file":"pages/api/alert-token.ts","language":"ts","sessionNote":"Replace `getSession` with the app's session lookup for API routes.","source":"import type { NextApiRequest, NextApiResponse } from \"next\";\nimport { getSession } from \"next-auth/react\"; // or your session helper\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  const session = await getSession({ req });\n  const email = session?.user?.email;\n  if (!email) return res.status(401).json({ error: \"not_authenticated\" });\n\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL ?? \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY ?? \"\";\n  if (!alertUrl || !serverKey) return res.status(500).json({ error: \"alert_not_configured\" });\n\n  try {\n    const r = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n    });\n    if (!r.ok) return res.status(502).json({ error: \"token_mint_failed\" });\n    return res.status(200).json(await r.json());\n  } catch {\n    return res.status(502).json({ error: \"alert_unreachable\" });\n  }\n}"},{"framework":"Express (Node)","file":"routes/alert-token.js","language":"js","sessionNote":"Replace `req.user?.email` with however your app exposes the logged-in user (Passport, session middleware, JWT, etc.).","source":"// app.get(\"/api/alert-token\", alertTokenHandler)\nmodule.exports = async function alertTokenHandler(req, res) {\n  const email = req.user?.email; // <-- your real session\n  if (!email) return res.status(401).json({ error: \"not_authenticated\" });\n\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL || \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY || \"\";\n  if (!alertUrl || !serverKey) return res.status(500).json({ error: \"alert_not_configured\" });\n\n  try {\n    const r = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n    });\n    if (!r.ok) return res.status(502).json({ error: \"token_mint_failed\" });\n    return res.json(await r.json());\n  } catch {\n    return res.status(502).json({ error: \"alert_unreachable\" });\n  }\n};"},{"framework":"Fastify (Node)","file":"plugins/alert-token.js","language":"js","sessionNote":"Replace `request.user?.email` with your auth plugin's user.","source":"// fastify.get(\"/api/alert-token\", alertTokenHandler)\nmodule.exports = async function alertTokenHandler(request, reply) {\n  const email = request.user?.email; // <-- your real session\n  if (!email) return reply.code(401).send({ error: \"not_authenticated\" });\n\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL || \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY || \"\";\n  if (!alertUrl || !serverKey) return reply.code(500).send({ error: \"alert_not_configured\" });\n\n  const r = await fetch(alertUrl + \"/api/embed/session-token\", {\n    method: \"POST\",\n    headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ user: email }),\n  });\n  if (!r.ok) return reply.code(502).send({ error: \"token_mint_failed\" });\n  return reply.send(await r.json());\n};"},{"framework":"Django","file":"views.py (mapped to /api/alert-token)","language":"python","sessionNote":"Uses request.user.email from Django auth; adjust if you use a custom user model.","source":"import os, json, urllib.request\nfrom django.http import JsonResponse\n\ndef alert_token(request):\n    if not request.user.is_authenticated:\n        return JsonResponse({\"error\": \"not_authenticated\"}, status=401)\n    email = request.user.email\n\n    alert_url = os.environ.get(\"NEXT_PUBLIC_ALERTS_URL\", \"\").rstrip(\"/\")\n    server_key = os.environ.get(\"ALERT_SERVER_KEY\", \"\")\n    if not alert_url or not server_key:\n        return JsonResponse({\"error\": \"alert_not_configured\"}, status=500)\n\n    req = urllib.request.Request(\n        alert_url + \"/api/embed/session-token\",\n        data=json.dumps({\"user\": email}).encode(),\n        headers={\"Authorization\": \"Bearer \" + server_key, \"Content-Type\": \"application/json\"},\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(req, timeout=8) as resp:\n            return JsonResponse(json.loads(resp.read()))\n    except Exception:\n        return JsonResponse({\"error\": \"token_mint_failed\"}, status=502)"},{"framework":"Flask","file":"app.py (route /api/alert-token)","language":"python","sessionNote":"Uses flask_login current_user; adjust to your session.","source":"import os, requests\nfrom flask import jsonify\nfrom flask_login import current_user\n\n@app.get(\"/api/alert-token\")\ndef alert_token():\n    if not current_user.is_authenticated:\n        return jsonify(error=\"not_authenticated\"), 401\n    email = current_user.email\n\n    alert_url = os.environ.get(\"NEXT_PUBLIC_ALERTS_URL\", \"\").rstrip(\"/\")\n    server_key = os.environ.get(\"ALERT_SERVER_KEY\", \"\")\n    if not alert_url or not server_key:\n        return jsonify(error=\"alert_not_configured\"), 500\n    try:\n        r = requests.post(alert_url + \"/api/embed/session-token\",\n                          headers={\"Authorization\": \"Bearer \" + server_key},\n                          json={\"user\": email}, timeout=8)\n        if not r.ok:\n            return jsonify(error=\"token_mint_failed\"), 502\n        return jsonify(r.json())\n    except Exception:\n        return jsonify(error=\"alert_unreachable\"), 502"},{"framework":"Generic (any server)","file":"an endpoint at GET /api/alert-token","language":"text","sessionNote":"Whatever your stack: read the logged-in user's email from your session, POST it to Alert with the server key, return the JSON.","source":"GET /api/alert-token  (server-side, same origin as your app)\n  1. Read the logged-in user's identifier (e.g. email) from your session.\n  2. POST to {ALERT_URL}/api/embed/session-token\n       Header:  Authorization: Bearer {ALERT_SERVER_KEY}\n       Body:    { \"user\": \"<that email>\" }\n  3. Return the JSON response ({ token, expiresIn }) to the browser.\n  Never expose ALERT_SERVER_KEY to the client."}]},{"n":6,"title":"Place the bell (frontend)","detail":"Read the app's real layout/header/nav component and add the bell there so it appears on every authenticated page. Prefer a mounted placement in the top nav via data-mount targeting an element you add near the user avatar; otherwise let it float. For React/Next.js, inject the script via a small client component in the layout (do not paste a raw <script> into JSX). For plain HTML, paste the tag before </body>.","snippet":"<script src=\"https://localhost:3000/embed/bell.js\"\n        data-token-url=\"/api/alert-token\"\n        async></script>","mountedVariant":"<script src=\"https://localhost:3000/embed/bell.js\"\n        data-token-url=\"/api/alert-token\"\n        data-mount=\"#alert-bell-slot\"\n        async></script>","reactComponent":"'use client';\nimport { useEffect } from 'react';\nexport default function AlertBellWidget() {\n  useEffect(() => {\n    const url = (process.env.NEXT_PUBLIC_ALERTS_URL ?? '').replace(/\\/+$/, '');\n    if (!url || document.querySelector('script[data-alert-bell-embed]')) return;\n    const s = document.createElement('script');\n    s.src = url + '/embed/bell.js';\n    s.async = true;\n    s.setAttribute('data-alert-bell-embed', '');\n    s.setAttribute('data-token-url', '/api/alert-token');\n    s.setAttribute('data-mount', '#alert-bell-slot');\n    document.body.appendChild(s);\n  }, []);\n  return <span id=\"alert-bell-slot\" style={{ display: 'inline-flex', alignItems: 'center' }} />;\n}"},{"n":7,"title":"Build, then validate the token route (the critical check)","detail":"Run the app's build/typecheck and fix anything you broke (if you can't fix it in 2 tries, revert and report). Start the app with a user logged in. Then: (a) POST verify again — alreadyLive should be true (bell is loading). (b) CRITICAL: call the app's own /api/alert-token (with the logged-in session) to get a token, then POST that token to /api/embed/check-token — it must return valid:true for the correct workspace. This proves the token route is wired to the right key + session, which is the failure most integrations miss.","request":{"method":"POST","url":"https://localhost:3000/api/embed/check-token","body":{"token":"<token from the app's /api/alert-token>"}}},{"n":8,"title":"Prove it end-to-end (send a real test notification)","detail":"Fire a real notification to the logged-in user and confirm it actually appears in their bell — this proves the whole path works, not just that the bell connected. POST /api/embed/test-notification with { user: <the logged-in email> }. It returns a notificationId. Confirm receipt either by observing the bell in the running app, or by GET /api/notifications (with that user's token or the key + X-User-Id) and checking an item with that id is present. Tell the human it's a test they can dismiss.","request":{"method":"POST","url":"https://localhost:3000/api/embed/test-notification","headers":{"Authorization":"Bearer <ALERT_SERVER_KEY>"},"body":{"user":"<logged-in email>"}}},{"n":9,"title":"(Optional) Send events","detail":"If the app should generate notifications from its own server events, add calls like the snippet below at the points where those events occur. Triggers configured in Alert decide who gets notified.","snippet":"// Fire from your server whenever something happens; triggers decide who is notified.\nawait fetch(\"https://localhost:3000/api/events\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + process.env.ALERT_SERVER_KEY },\n  body: JSON.stringify({ event: \"task_update\", status: \"failed\", priority: \"P1\", title: \"Nightly sync\", assignee: \"alice@example.com\" }),\n});"}],"endpoints":{"verify":{"method":"POST","url":"https://localhost:3000/api/embed/verify","body":{"key":"string","appUrl":"string"}},"diagnose":{"method":"POST","url":"https://localhost:3000/api/embed/diagnose","body":{"key":"string","appUrl":"string"},"note":"Deep health check for an EXISTING integration: checks[] + repairPlan[]. Read-only, safe to call anytime."},"config":{"method":"GET","url":"https://localhost:3000/api/embed/config","note":"Resolves the workspace from the request Origin header. Public, no key."},"sessionToken":{"method":"POST","url":"https://localhost:3000/api/embed/session-token","headers":{"Authorization":"Bearer <ALERT_SERVER_KEY>"},"body":{"user":"email"},"note":"Server-to-server only. Returns { token, expiresIn }."},"testNotification":{"method":"POST","url":"https://localhost:3000/api/embed/test-notification","headers":{"Authorization":"Bearer <ALERT_SERVER_KEY>"},"body":{"user":"email"},"note":"Sends a REAL test notification to that user and returns its notificationId. Use it to prove the bell receives notifications end-to-end."},"checkToken":{"method":"POST","url":"https://localhost:3000/api/embed/check-token","body":{"token":"string"},"note":"Validates a token produced by the APP's /api/alert-token route. Returns valid + which workspace/user. Use it to PROVE the app's token route is wired correctly — the most common silent failure."},"events":{"method":"POST","url":"https://localhost:3000/api/events","headers":{"Authorization":"Bearer <ALERT_SERVER_KEY>"}},"bell":{"url":"https://localhost:3000/embed/bell.js","note":"Hosted keyless widget. Configure via data-* attributes."}},"modes":{"newIntegration":{"when":"The app has never been connected to Alert.","procedure":"Follow steps 1-9 above. Step 8 (real test notification) is the definitive proof it works — don't declare success before it."},"repair":{"when":"The user says the bell isn't working, notifications aren't arriving, setup was left half-done, or asks to 'fix'/'check' the Alert integration.","procedure":["Call POST /api/embed/diagnose with { key, appUrl }. This is read-only and safe to call anytime, even mid-investigation.","Read `checks[]`. Each has status ok|warn|fail with a `fix` hint. Read `repairPlan[]` for the ordered list of what to actually do.","Fix only what diagnose flagged. Do not rewrite working code. Do not touch the bell placement if bell_live is already ok.","Typical fixes: domain not registered/mismatched -> tell the human to fix in Alert UI (agents cannot register a domain themselves, only company admins can); token route missing/erroring -> create/fix it; bell script missing -> add it; env vars missing -> add them.","Re-run diagnose after each fix until `healthy: true` (or only non-critical warnings remain, e.g. bell not live because no one is on the app right now)."]},"rotateKey":{"when":"The user asks to rotate/change/replace the API key (security review, suspected leak, routine rotation).","safety":"This is a NO-DOWNTIME rotation. The old key keeps working until explicitly revoked, so never revoke before confirming the new key.","procedure":["Ask a company admin to run POST /api/company/workspaces/{workspaceId}/keys/rotate (requires an Alert company session — an agent cannot call this directly). It returns { newKey: { key }, oldKeyId, domainCarriedOver }.","Update the app's .env: replace ALERT_SERVER_KEY with newKey.key. Do not remove NEXT_PUBLIC_ALERTS_URL.","Restart/redeploy the app so the new key is loaded.","Confirm the new key works: POST /api/embed/verify with the NEW key and the app URL. Require ready: true before continuing.","Only after verify confirms the new key works, have the admin revoke the OLD key via DELETE /api/company/workspaces/{workspaceId}/keys/{oldKeyId} (the exact URL is returned as `revokeOldKeyUrl` by the rotate call).","Never revoke the old key before the new key is verified working — that would cause an outage."]},"migrateEnv":{"when":"Moving the app from one Alert environment to another (e.g. QC to production), or the app's own URL changed.","procedure":["Get the new ALERT_URL and a server key that belongs to a workspace in that environment (a company admin creates/connects the app there if it doesn't exist yet — see Connect an App).","POST /api/embed/verify against the NEW ALERT_URL with the new key and the app's (new) URL. Fix any domainRegistered/domainMatches issues first.","Update .env: NEXT_PUBLIC_ALERTS_URL and ALERT_SERVER_KEY to the new environment's values.","The token route and bell <script> do not need to change (they read from env), unless the bell snippet hardcodes an old ALERT_URL as data-service-url — if so, update it or remove the attribute so it defaults to the script's own origin.","Build, redeploy, and confirm via verify/diagnose against the new environment."]}},"deployment":{"summary":"You run this integration ONCE. It makes code changes (token route, bell in the layout, env references, any middleware exclusion) that you COMMIT to the repo. You do NOT re-run it on every deploy — every future deployment just ships the committed code. This is the key difference from the old SDK, which re-ran a patch script on every build.","onceCommitted":["The token route file (app/api/alert-token/route.ts or equivalent)","The bell in the layout (the client component + its mount)","The middleware exclusion for /api/alert-token","These live in the repo and deploy like any other code — no re-run."],"perEnvironment":["Only two values differ per environment, and they are ENV VARS, not code — set them in your existing deploy config (IIS App Settings / ECS task def / .env.qc / .env.prod), exactly like your DB URL or Cognito secrets:","NEXT_PUBLIC_ALERTS_URL — e.g. https://alertqc.compucom.com (QC) vs https://alert.compucom.com (prod).","ALERT_SERVER_KEY — the server key for the workspace registered in THAT Alert environment (QC key for QC, prod key for prod). Each environment's app URL must be registered as the domain on its key in that Alert environment.","The token route reads these from env at runtime with NO hardcoded fallback, so the same committed code works in every environment just by changing the env vars."],"note":"When promoting an app from QC to prod, you do NOT re-run the AI integration. You register the prod app URL in prod Alert (Connect an App), set the prod NEXT_PUBLIC_ALERTS_URL + ALERT_SERVER_KEY in the prod deploy config, and ship the same code."},"safety":["Diagnose is read-only — call it as often as needed without risk.","Never revoke a key before the replacement key is verified working (no-downtime rule).","Never expose ALERT_SERVER_KEY to the browser, logs, or commit it to source control.","Prefer additive, reversible changes. If you must edit an existing working file, keep the diff minimal and explain what changed.","An agent cannot register/change a domain or rotate a key on its own — those require an Alert company admin session. The agent's job is to guide the human to do that specific action, then continue.","Always re-verify (verify or diagnose) after a change before declaring success. Never claim an integration works without a fresh check."],"bellAttributes":{"data-token-url":"Path on the app that returns { token } (default /api/alert-token). Required in keyless mode.","data-service-url":"Alert origin. Defaults to the origin the script is served from.","data-mount":"CSS selector to place the bell into. Omit to float in a corner.","data-position":"bottom-right | bottom-left | top-right | top-left (when floating).","data-accent":"Hex accent color for the primary action button.","data-sound":"on | off (chime on new notifications)."},"troubleshooting":[{"symptom":"Bell shows 'Not connected'","cause":"Domain not registered, or app URL != registered domain.","fix":"Register/adjust the domain in Alert (Connect an App / API Keys tab), or pass the correct app URL. Re-run verify."},{"symptom":"Bell shows but no notifications, 'All caught up'","cause":"No notifications for this user yet, or no triggers.","fix":"Send a test via Compose in Alert, or add triggers so events route to users."},{"symptom":"Token route returns 401","cause":"The app's session isn't available server-side in the route.","fix":"Ensure the token route reads the logged-in user's identity from the app's real session helper."},{"symptom":"Bell stays Offline; console shows \"Unexpected token '<', \\\"<!DOCTYPE\\\"... is not valid JSON\"","cause":"The app's auth middleware is redirecting /api/alert-token (and/or the SSE stream calls) to the login HTML page, so the bell receives HTML instead of JSON.","fix":"Exclude /api/alert-token from the app's auth middleware/redirect (e.g. add it to the Next.js middleware matcher's negative-lookahead exclusions, next to api/auth). The token route authenticates itself and returns proper JSON."},{"symptom":"SSE stream never goes Live behind IIS/Cloudflare","cause":"Proxy buffering.","fix":"IIS: ensure responseBufferLimit/flush is not buffering text/event-stream. Cloudflare: bypass cache for /api/notifications/stream. The widget also has a 3s DB-poll fallback."},{"symptom":"Styles clash with the host page","cause":"None expected — the bell renders in a Shadow DOM.","fix":"No action needed; host CSS cannot affect the bell and vice versa."}],"doNot":["Do not expose ALERT_SERVER_KEY to the browser (no NEXT_PUBLIC_ prefix, no client component).","Do not hardcode or trust a user id from the client; always derive it server-side.","Do not install the old SDK folder or patch components with regex; place the bell by reading the real layout."],"templates":{"bellSnippet":"<script src=\"https://localhost:3000/embed/bell.js\"\n        data-token-url=\"/api/alert-token\"\n        async></script>","tokenRouteSource":"import { NextResponse } from \"next/server\";\nimport { auth } from \"@/auth\"; // your existing SSO/session helper\n\nexport async function GET() {\n  const session = await auth();\n  const email = session?.user?.email;\n  if (!email) return NextResponse.json({ error: \"not_authenticated\" }, { status: 401 });\n\n  // Read from env per environment (QC vs prod) — no hardcoded fallback.\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL ?? \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY ?? \"\";\n  if (!alertUrl || !serverKey) return NextResponse.json({ error: \"alert_not_configured\" }, { status: 500 });\n\n  try {\n    const res = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n      cache: \"no-store\",\n    });\n    if (!res.ok) return NextResponse.json({ error: \"token_mint_failed\" }, { status: 502 });\n    return NextResponse.json(await res.json());\n  } catch {\n    return NextResponse.json({ error: \"alert_unreachable\" }, { status: 502 });\n  }\n}","sendEventSnippet":"// Fire from your server whenever something happens; triggers decide who is notified.\nawait fetch(\"https://localhost:3000/api/events\", {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\", \"Authorization\": \"Bearer \" + process.env.ALERT_SERVER_KEY },\n  body: JSON.stringify({ event: \"task_update\", status: \"failed\", priority: \"P1\", title: \"Nightly sync\", assignee: \"alice@example.com\" }),\n});","reactComponent":"'use client';\nimport { useEffect } from 'react';\nexport default function AlertBellWidget() {\n  useEffect(() => {\n    const url = (process.env.NEXT_PUBLIC_ALERTS_URL ?? '').replace(/\\/+$/, '');\n    if (!url || document.querySelector('script[data-alert-bell-embed]')) return;\n    const s = document.createElement('script');\n    s.src = url + '/embed/bell.js';\n    s.async = true;\n    s.setAttribute('data-alert-bell-embed', '');\n    s.setAttribute('data-token-url', '/api/alert-token');\n    s.setAttribute('data-mount', '#alert-bell-slot');\n    document.body.appendChild(s);\n  }, []);\n  return <span id=\"alert-bell-slot\" style={{ display: 'inline-flex', alignItems: 'center' }} />;\n}","tokenRouteTemplates":[{"framework":"Next.js (App Router)","file":"app/api/alert-token/route.ts","language":"ts","sessionNote":"Replace `auth()` with the app's real session helper (next-auth v5 `auth()`, `getServerSession(authOptions)`, Clerk `auth()`, etc.).","source":"import { NextResponse } from \"next/server\";\nimport { auth } from \"@/auth\"; // your existing SSO/session helper\n\nexport async function GET() {\n  const session = await auth();\n  const email = session?.user?.email;\n  if (!email) return NextResponse.json({ error: \"not_authenticated\" }, { status: 401 });\n\n  // Read from env per environment (QC vs prod) — no hardcoded fallback.\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL ?? \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY ?? \"\";\n  if (!alertUrl || !serverKey) return NextResponse.json({ error: \"alert_not_configured\" }, { status: 500 });\n\n  try {\n    const res = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n      cache: \"no-store\",\n    });\n    if (!res.ok) return NextResponse.json({ error: \"token_mint_failed\" }, { status: 502 });\n    return NextResponse.json(await res.json());\n  } catch {\n    return NextResponse.json({ error: \"alert_unreachable\" }, { status: 502 });\n  }\n}"},{"framework":"Next.js (Pages Router)","file":"pages/api/alert-token.ts","language":"ts","sessionNote":"Replace `getSession` with the app's session lookup for API routes.","source":"import type { NextApiRequest, NextApiResponse } from \"next\";\nimport { getSession } from \"next-auth/react\"; // or your session helper\n\nexport default async function handler(req: NextApiRequest, res: NextApiResponse) {\n  const session = await getSession({ req });\n  const email = session?.user?.email;\n  if (!email) return res.status(401).json({ error: \"not_authenticated\" });\n\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL ?? \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY ?? \"\";\n  if (!alertUrl || !serverKey) return res.status(500).json({ error: \"alert_not_configured\" });\n\n  try {\n    const r = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n    });\n    if (!r.ok) return res.status(502).json({ error: \"token_mint_failed\" });\n    return res.status(200).json(await r.json());\n  } catch {\n    return res.status(502).json({ error: \"alert_unreachable\" });\n  }\n}"},{"framework":"Express (Node)","file":"routes/alert-token.js","language":"js","sessionNote":"Replace `req.user?.email` with however your app exposes the logged-in user (Passport, session middleware, JWT, etc.).","source":"// app.get(\"/api/alert-token\", alertTokenHandler)\nmodule.exports = async function alertTokenHandler(req, res) {\n  const email = req.user?.email; // <-- your real session\n  if (!email) return res.status(401).json({ error: \"not_authenticated\" });\n\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL || \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY || \"\";\n  if (!alertUrl || !serverKey) return res.status(500).json({ error: \"alert_not_configured\" });\n\n  try {\n    const r = await fetch(alertUrl + \"/api/embed/session-token\", {\n      method: \"POST\",\n      headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n      body: JSON.stringify({ user: email }),\n    });\n    if (!r.ok) return res.status(502).json({ error: \"token_mint_failed\" });\n    return res.json(await r.json());\n  } catch {\n    return res.status(502).json({ error: \"alert_unreachable\" });\n  }\n};"},{"framework":"Fastify (Node)","file":"plugins/alert-token.js","language":"js","sessionNote":"Replace `request.user?.email` with your auth plugin's user.","source":"// fastify.get(\"/api/alert-token\", alertTokenHandler)\nmodule.exports = async function alertTokenHandler(request, reply) {\n  const email = request.user?.email; // <-- your real session\n  if (!email) return reply.code(401).send({ error: \"not_authenticated\" });\n\n  const alertUrl = (process.env.NEXT_PUBLIC_ALERTS_URL || \"\").replace(/\\/+$/, \"\");\n  const serverKey = process.env.ALERT_SERVER_KEY || \"\";\n  if (!alertUrl || !serverKey) return reply.code(500).send({ error: \"alert_not_configured\" });\n\n  const r = await fetch(alertUrl + \"/api/embed/session-token\", {\n    method: \"POST\",\n    headers: { Authorization: \"Bearer \" + serverKey, \"Content-Type\": \"application/json\" },\n    body: JSON.stringify({ user: email }),\n  });\n  if (!r.ok) return reply.code(502).send({ error: \"token_mint_failed\" });\n  return reply.send(await r.json());\n};"},{"framework":"Django","file":"views.py (mapped to /api/alert-token)","language":"python","sessionNote":"Uses request.user.email from Django auth; adjust if you use a custom user model.","source":"import os, json, urllib.request\nfrom django.http import JsonResponse\n\ndef alert_token(request):\n    if not request.user.is_authenticated:\n        return JsonResponse({\"error\": \"not_authenticated\"}, status=401)\n    email = request.user.email\n\n    alert_url = os.environ.get(\"NEXT_PUBLIC_ALERTS_URL\", \"\").rstrip(\"/\")\n    server_key = os.environ.get(\"ALERT_SERVER_KEY\", \"\")\n    if not alert_url or not server_key:\n        return JsonResponse({\"error\": \"alert_not_configured\"}, status=500)\n\n    req = urllib.request.Request(\n        alert_url + \"/api/embed/session-token\",\n        data=json.dumps({\"user\": email}).encode(),\n        headers={\"Authorization\": \"Bearer \" + server_key, \"Content-Type\": \"application/json\"},\n        method=\"POST\",\n    )\n    try:\n        with urllib.request.urlopen(req, timeout=8) as resp:\n            return JsonResponse(json.loads(resp.read()))\n    except Exception:\n        return JsonResponse({\"error\": \"token_mint_failed\"}, status=502)"},{"framework":"Flask","file":"app.py (route /api/alert-token)","language":"python","sessionNote":"Uses flask_login current_user; adjust to your session.","source":"import os, requests\nfrom flask import jsonify\nfrom flask_login import current_user\n\n@app.get(\"/api/alert-token\")\ndef alert_token():\n    if not current_user.is_authenticated:\n        return jsonify(error=\"not_authenticated\"), 401\n    email = current_user.email\n\n    alert_url = os.environ.get(\"NEXT_PUBLIC_ALERTS_URL\", \"\").rstrip(\"/\")\n    server_key = os.environ.get(\"ALERT_SERVER_KEY\", \"\")\n    if not alert_url or not server_key:\n        return jsonify(error=\"alert_not_configured\"), 500\n    try:\n        r = requests.post(alert_url + \"/api/embed/session-token\",\n                          headers={\"Authorization\": \"Bearer \" + server_key},\n                          json={\"user\": email}, timeout=8)\n        if not r.ok:\n            return jsonify(error=\"token_mint_failed\"), 502\n        return jsonify(r.json())\n    except Exception:\n        return jsonify(error=\"alert_unreachable\"), 502"},{"framework":"Generic (any server)","file":"an endpoint at GET /api/alert-token","language":"text","sessionNote":"Whatever your stack: read the logged-in user's email from your session, POST it to Alert with the server key, return the JSON.","source":"GET /api/alert-token  (server-side, same origin as your app)\n  1. Read the logged-in user's identifier (e.g. email) from your session.\n  2. POST to {ALERT_URL}/api/embed/session-token\n       Header:  Authorization: Bearer {ALERT_SERVER_KEY}\n       Body:    { \"user\": \"<that email>\" }\n  3. Return the JSON response ({ token, expiresIn }) to the browser.\n  Never expose ALERT_SERVER_KEY to the client."}]}}