cd /news/ai-tools/adding-groq-powered-ai-summaries-to-… · home › topics › ai-tools › article
[ARTICLE · art-152442] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Adding Groq‑Powered AI Summaries to the ISO‑27001 Dashboard with Daily Usage Limits

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.

by read4 min views2 publishedOct 18, 2026

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.

The 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.

Two technical blockers surfaced:

The 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.

My first attempt was a client‑side fetch from components/ai-button.tsx:

// early prototype (removed)
const resp = await fetch("https://api.groq.com/openai/v1/chat/completions", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_GROQ_KEY}` },
  body: JSON.stringify(payload),
});

Issues:

NEXT_PUBLIC_GROQ_KEY leaked into the bundle, visible in Chrome dev tools. localhost:3000. I 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.

Result: the button either failed with a 401 or kept consuming tokens until the daily budget was spent, after which every call returned a 429.

Added the Groq key to .env.example (never committed the real value):

+ GROQ_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ AI_DAILY_LIMIT=2000   # token budget per day

The production .env now contains the real key, and the code only accesses it via process.env.GROQ_API_KEY on the server.

lib/ai.ts) Created a thin wrapper around the Groq client that also tracks daily usage:

// lib/ai.ts
import { Groq } from "groq-sdk";
import { db } from "@/lib/db";

export const groq = new Groq({ apiKey: process.env.GROQ_API_KEY });

export const aiEnabled = process.env.GROQ_API_KEY !== undefined;
export const dailyLimit = Number(process.env.AI_DAILY_LIMIT ?? 0);

/** Returns true if we have quota left for today */
export async function canUseAi(): Promise<boolean> {
  const { used } = await db
    .selectFrom("ai_usage")
    .select("used")
    .where("date", "=", new Date().toISOString().slice(0, 10))
    .executeTakeFirst();

  return (used ?? 0) < dailyLimit;
}

/** Increment usage counter */
export async function recordAiUsage(tokens: number) {
  await db
    .insertInto("ai_usage")
    .values({
      date: new Date().toISOString().slice(0, 10),
      used: db.raw(`COALESCE(used,0) + ${tokens}`),
    })
    .onConflictDoUpdate({
      target: ["date"],
      set: { used: db.raw(`ai_usage.used + ${tokens}`) },
    })
    .execute();
}

The ai_usage table is defined in sql/schema.sql:

+ CREATE TABLE ai_usage (
+   date DATE PRIMARY KEY,
+   used INTEGER NOT NULL DEFAULT 0
+ );

app/(app)/analisis/actions.ts) Next.js 13 server actions let us call the AI from a form without an explicit API route:

// app/(app)/analisis/actions.ts
"use server";

import { redirect } from "next/navigation";
import { RuleError, str, withParam, errorMessage } from "@/lib/action-util";
import { aiEnabled, aiModel, buildContext, canUseAi, recordAiUsage, groq } from "@/lib/ai";

export async function generateSummary(id: string) {
  if (!aiEnabled) throw new RuleError("AI not configured");

  if (!(await canUseAi())) {
    throw new RuleError("Daily AI quota exhausted");
  }

  const context = await buildContext(id);
  const resp = await groq.chat.completions.create({
    model: aiModel,
    messages: [{ role: "system", content: context }],
  });

  const tokens = resp.usage?.total_tokens ?? 0;
  await recordAiUsage(tokens);

  // Store the result in the DB (pseudo code)
  await db
    .updateTable("analisis")
    .set({ ai_summary: resp.choices[0].message.content })
    .where("id", "=", id)
    .execute();

  redirect(`/analisis/${id}`);
}

Key points:

"use server"`` canUseAi() checks the daily limit before any request. recordAiUsage() updates the ai_usage table atomically. RuleError and surfaced by the UI component.components/ai-button.tsx

// components/ai-button.tsx
import { useTransition } from "react";
import { generateSummary } from "@/app/(app)/analisis/actions";

export function AiButton({ id }: { id: string }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() => startTransition(() => generateSummary(id))}
      className="btn-primary"
    >
      {isPending ? "Generating…" : "Generate AI Summary"}
    </button>
  );
}

The button triggers the server action directly, avoiding a separate fetch call.

components/ai-text.tsx

// components/ai-text.tsx
export function AiText({ text }: { text: string }) {
  return <p className="prose prose-sm mt-2">{text}</p>;
}

Used in the analysis detail page to render the stored summary.

Added the button and text to the analysis detail page:

diff
+ import { AiButton } from "@/components/ai-button";
+ import { AiText } from "@/components/ai-text";
...
+ <AiButton id={params.id} />
+ {analysis.ai_summary &&

---

*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.*

*Repo: `zaerohell/iso27001-tablero` · 2026-10-17*

\#playadev #buildinpublic
── more in #ai-tools 4 stories · sorted by recency
── more on @groq 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/adding-groq-powered-…] indexed:0 read:4min 2026-10-18 · —