{"slug": "adding-groq-powered-ai-summaries-to-the-iso-27001-dashboard-with-daily-usage", "title": "Adding Groq‑Powered AI Summaries to the ISO‑27001 Dashboard with Daily Usage Limits", "summary": "A developer added server-side Groq-powered AI summaries to an ISO-27001 compliance dashboard, moving the Groq API key out of the client bundle and into server-only environment variables to fix a 401 Unauthorized leak. A usage-counter middleware backed by a new ai_usage table enforces a per-day token quota (AI_DAILY_LIMIT, set to 2000) and returns a quota-exhausted error instead of letting requests hit Groq's 429 rate limit. The summaries are generated through Next.js 13 server actions and surfaced via AiButton and AiText components without a full page reload.", "body_md": "**TL;DR:** Integrated server‑side Groq calls to generate daily AI summaries for each analysis page, added a usage‑counter middleware and UI components (`AiButton`, `AiText`). This keeps API keys secret, enforces a per‑day quota, and surfaces results without a full page reload.\n\nThe ISO‑27001 dashboard lets auditors view controls, policies, and daily logs. A recurring request from the team was to get an AI‑generated summary of each *analysis* (`/analisis/[id]`) that highlighted gaps and suggested next steps.  \n\nTwo technical blockers surfaced:\n\nThe symptom was a broken “Generate Summary” button that either threw a **401 Unauthorized** (key exposed) or let anyone hammer the endpoint until the quota was exhausted, resulting in **429 Too Many Requests** from Groq.\n\nMy first attempt was a **client‑side fetch** from `components/ai-button.tsx`:\n\n``` js\n// early prototype (removed)\nconst resp = await fetch(\"https://api.groq.com/openai/v1/chat/completions\", {\n  method: \"POST\",\n  headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_GROQ_KEY}` },\n  body: JSON.stringify(payload),\n});\n```\n\nIssues:\n\n`NEXT_PUBLIC_GROQ_KEY` leaked into the bundle, visible in Chrome dev tools.\n`localhost:3000`.\nI also tried wrapping the fetch in a **Next.js API route** (`pages/api/ai.ts`) but hit the same quota‑blow problem because the route lacked any persistence of usage counts.\n\nResult: the button either failed with a 401 or kept consuming tokens until the daily budget was spent, after which every call returned a 429.\n\nAdded the Groq key to `.env.example` (never committed the real value):\n\n```\n+ GROQ_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx\n+ AI_DAILY_LIMIT=2000   # token budget per day\n```\n\nThe production `.env` now contains the real key, and the code only accesses it via `process.env.GROQ_API_KEY` on the server.\n\n`lib/ai.ts`)\nCreated a thin wrapper around the Groq client that also tracks daily usage:\n\n``` js\n// lib/ai.ts\nimport { Groq } from \"groq-sdk\";\nimport { db } from \"@/lib/db\";\n\nexport const groq = new Groq({ apiKey: process.env.GROQ_API_KEY });\n\nexport const aiEnabled = process.env.GROQ_API_KEY !== undefined;\nexport const dailyLimit = Number(process.env.AI_DAILY_LIMIT ?? 0);\n\n/** Returns true if we have quota left for today */\nexport async function canUseAi(): Promise<boolean> {\n  const { used } = await db\n    .selectFrom(\"ai_usage\")\n    .select(\"used\")\n    .where(\"date\", \"=\", new Date().toISOString().slice(0, 10))\n    .executeTakeFirst();\n\n  return (used ?? 0) < dailyLimit;\n}\n\n/** Increment usage counter */\nexport async function recordAiUsage(tokens: number) {\n  await db\n    .insertInto(\"ai_usage\")\n    .values({\n      date: new Date().toISOString().slice(0, 10),\n      used: db.raw(`COALESCE(used,0) + ${tokens}`),\n    })\n    .onConflictDoUpdate({\n      target: [\"date\"],\n      set: { used: db.raw(`ai_usage.used + ${tokens}`) },\n    })\n    .execute();\n}\n```\n\nThe `ai_usage` table is defined in `sql/schema.sql`:\n\n```\n+ CREATE TABLE ai_usage (\n+   date DATE PRIMARY KEY,\n+   used INTEGER NOT NULL DEFAULT 0\n+ );\n```\n\n`app/(app)/analisis/actions.ts`)\nNext.js 13 server actions let us call the AI from a form without an explicit API route:\n\n``` js\n// app/(app)/analisis/actions.ts\n\"use server\";\n\nimport { redirect } from \"next/navigation\";\nimport { RuleError, str, withParam, errorMessage } from \"@/lib/action-util\";\nimport { aiEnabled, aiModel, buildContext, canUseAi, recordAiUsage, groq } from \"@/lib/ai\";\n\nexport async function generateSummary(id: string) {\n  if (!aiEnabled) throw new RuleError(\"AI not configured\");\n\n  if (!(await canUseAi())) {\n    throw new RuleError(\"Daily AI quota exhausted\");\n  }\n\n  const context = await buildContext(id);\n  const resp = await groq.chat.completions.create({\n    model: aiModel,\n    messages: [{ role: \"system\", content: context }],\n  });\n\n  const tokens = resp.usage?.total_tokens ?? 0;\n  await recordAiUsage(tokens);\n\n  // Store the result in the DB (pseudo code)\n  await db\n    .updateTable(\"analisis\")\n    .set({ ai_summary: resp.choices[0].message.content })\n    .where(\"id\", \"=\", id)\n    .execute();\n\n  redirect(`/analisis/${id}`);\n}\n```\n\nKey points:\n\n`\"use server\"`` canUseAi()` checks the daily limit before any request.\n`recordAiUsage()` updates the `ai_usage` table atomically.\n`RuleError` and surfaced by the UI component.`components/ai-button.tsx`\n\n``` js\n// components/ai-button.tsx\nimport { useTransition } from \"react\";\nimport { generateSummary } from \"@/app/(app)/analisis/actions\";\n\nexport function AiButton({ id }: { id: string }) {\n  const [isPending, startTransition] = useTransition();\n\n  return (\n    <button\n      disabled={isPending}\n      onClick={() => startTransition(() => generateSummary(id))}\n      className=\"btn-primary\"\n    >\n      {isPending ? \"Generating…\" : \"Generate AI Summary\"}\n    </button>\n  );\n}\n```\n\nThe button triggers the server action directly, avoiding a separate fetch call.\n\n`components/ai-text.tsx`\n\n```\n// components/ai-text.tsx\nexport function AiText({ text }: { text: string }) {\n  return <p className=\"prose prose-sm mt-2\">{text}</p>;\n}\n```\n\nUsed in the analysis detail page to render the stored summary.\n\nAdded the button and text to the analysis detail page:\n\n``` js\ndiff\n+ import { AiButton } from \"@/components/ai-button\";\n+ import { AiText } from \"@/components/ai-text\";\n...\n+ <AiButton id={params.id} />\n+ {analysis.ai_summary &&\n\n---\n\n*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*\n\n*Repo: `zaerohell/iso27001-tablero` · 2026-10-17*\n\n\\#playadev #buildinpublic\n```\n\n", "url": "https://wpnews.pro/news/adding-groq-powered-ai-summaries-to-the-iso-27001-dashboard-with-daily-usage", "canonical_source": "https://dev.to/zaerohell/adding-groq-powered-ai-summaries-to-the-iso-27001-dashboard-with-daily-usage-limits-559l", "published_at": "2026-10-18 17:41:28+00:00", "updated_at": "2026-10-18 17:47:30.615370+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-products"], "entities": ["Groq", "Next.js", "ISO-27001"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/adding-groq-powered-ai-summaries-to-the-iso-27001-dashboard-with-daily-usage", "markdown": "https://wpnews.pro/news/adding-groq-powered-ai-summaries-to-the-iso-27001-dashboard-with-daily-usage.md", "text": "https://wpnews.pro/news/adding-groq-powered-ai-summaries-to-the-iso-27001-dashboard-with-daily-usage.txt", "jsonld": "https://wpnews.pro/news/adding-groq-powered-ai-summaries-to-the-iso-27001-dashboard-with-daily-usage.jsonld"}}