Live demo: https://insightpilot-jade.vercel.app
Ask a question in plain English. A planner agent turns it into a structured, schema-validated query plan; a critic agent reviews that plan and can send it back for revision; a deterministic executor runs it as a safe, parameterized query — the LLM never touches SQL directly; a narrator agent summarizes the result. The resulting widget is pinned to a live dashboard that updates in real time as new data arrives.
It's a multi-tenant analytics copilot for a fictional SaaS support/billing platform, modeled on the pattern of a dashboard-widget system built at a real production job — reports built by end users from natural language, not fixed to a handful of pre-built charts.
Sign in at /login with any of the seeded demo accounts (password demo1234 for all):
| Role | Scope | |
|---|---|---|
| Admin | admin@insightpilot.dev |
All organizations, via an org switcher; user management |
| Manager | manager@insightpilot.dev |
Own organization (Northwind Ops); can create/pin widgets |
| Viewer | viewer@insightpilot.dev |
Own organization (Northwind Ops); read-only |
Ask something like:
- "How many tickets are currently open?"
- "Weekly ticket volume as a line chart"
- "Ratio of resolved to total tickets"
- "Tickets by category as a pie chart"
- "Support workload as a pivot of priority vs status"
As a manager/admin, hit Simulate activity on the dashboard to insert fresh rows and watch pinned widgets update live via Supabase Realtime — no refresh needed.
Stack: Next.js 16 (App Router) · TypeScript · Postgres (Supabase) via Drizzle ORM · Supabase Realtime · NextAuth (Credentials/JWT) · Groq (multi-agent pipeline) · Tailwind CSS · Recharts · react-grid-layout
Data model: a seeded multi-tenant dataset — organizations, users (role-scoped: admin/manager/viewer), tickets, subscriptions, events, and the widgets a user pins to their dashboard (src/lib/db/schema.ts).
The agent pipeline (src/lib/agents/) is the core of the project:
- Planner (
planner.ts) — given the question and a fixed description of the available data (SCHEMA_DESCRIPTIONintypes.ts), asks Groq for a structured JSON plan: source table, metric, chart type, filters, grouping. The plan is validated against a Zod schema (WidgetPlanSchema) before anything downstream trusts it — the model can only choose from a whitelist of known fields and operators, it never writes SQL. - Critic (
critic.ts) — reviews the plan against the original question (right chart type for the intent? sane filters?) and can request one revision from the planner with specific feedback. - Executor (
src/lib/query/executor.ts) — deterministic, no LLM involved. Turns the validated plan into a parameterized Drizzle query by mapping whitelisted field names to real columns; every query is scoped to the caller's organization. - Narrator (
narrator.ts) — writes a one- or two-sentence plain-English summary of the result.
All four stages stream live to the UI over Server-Sent Events (/api/agent/plan), so the "thinking" is visible, not just the final answer.
Multi-tenancy & RBAC: every query is scoped server-side to an organization. Managers/viewers are hard-locked to their own org; admins pick an org via a cookie-backed switcher (src/lib/rbac.ts) — client-supplied org overrides are only ever honored for the admin role.
Accessibility: streaming agent output lands in an ARIA live region; every chart (ChartRenderer.tsx) renders alongside an accessible data table, not just an SVG; the widget grid is draggable/resizable by mouse but every resize also has keyboard-operable buttons, since drag-and-drop alone excludes keyboard users.
npm install
cp .env.local.example .env.local # fill in the values below
npm run db:push # create tables in your Postgres/Supabase project
npm run db:seed # seed demo orgs, users, and ~90 days of data
npm run dev| Variable | Where to get it |
|---|---|
DATABASE_URL |
Supabase project → Connect → Transaction pooler connection string (port 6543 — the app is configured for pooler mode, not a direct connection) |
NEXT_PUBLIC_SUPABASE_URL |
Supabase project → Settings → Data API → Project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase project → Settings → Data API → anon public key (never the service_role/secret key — this one ships to the browser) |
GROQ_API_KEY |
console.groq.com/keys (free tier) |
NEXTAUTH_SECRET |
Any random string, e.g. openssl rand -base64 32 |
NEXTAUTH_URL |
http://localhost:3000 locally; your deployed URL in production |
For live widget updates to work, enable Realtime replication on the tickets, subscriptions, and events tables (Supabase dashboard → Database → Replication).
npm run lint # eslint .
npm run build # production build
npm run db:studio # Drizzle Studio, to browse the seeded dataDeployed on Vercel with auto-deploy on push to main. The same environment variables above are set as Production env vars in the Vercel project; NEXTAUTH_URL is set to the deployed domain.
- Realtime scope: the Supabase Realtime subscription currently broadcasts row changes across all organizations, not filtered per-tenant at the database level — fine for this seeded demo, but a real multi-tenant product would add Row Level Security policies scoping Realtime broadcasts per org too.
- Narrator text is a snapshot: a pinned widget's chart value refreshes live, but its plain-English sentence is written once, when the widget is created, and stored as-is — regenerating it on every view would mean an extra LLM call on every dashboard load.
- Single free-tier Postgres connection pool: using Supabase's transaction pooler (not a direct connection) since serverless functions open/close connections far more often than a long-lived server would.