The Problem
Org owners buying AI seats for a team need three things a generic chat UI doesn't give them: a way to organize a team's conversations by client or project, cost control across a menu of LLMs at very different price points, and enough visibility into usage to justify the spend to their own leadership.
My Role
Solo developer, full stack — every collection, route, and library module in the app.
What I Built
- Folder-based conversation organization with per-folder access levels (private/shared/custom) and a default folder per member
- A model-tier selector (Auto/Fast/Expert) and an AI router behind Auto: a near-zero-cost heuristic classifier resolves most messages instantly, falling through to an LLM-as-judge call only for ambiguous ones, to pick between an economy and a capability model on OpenRouter
- Per-folder RAG: file/link ingestion, chunking, OpenRouter embeddings, and pgvector similarity search scoped to a folder
- An admin analytics dashboard aggregating token/cost by model, tier, member, and day, including a counterfactual savings comparison against always using the expensive tier
- Stripe billing across free/basic/pro/max plans, with org members inheriting the owner's plan
- Org and member management, invitations, and a banned-user hard-block enforcement path
Architecture
- 1
Next.js
Chat UI and admin dashboard
- 2
better-auth
Users, sessions, organizations
- 3
Payload CMS + PostgreSQL
Folders, chats, usage logs, RAG sources
- 4
Vercel AI SDK + OpenRouter
Streaming chat across model tiers
- 5
pgvector
Per-folder RAG retrieval
Next.js App Router with Payload CMS on Postgres for everything except identity — better-auth, with its organization plugin, owns users, sessions, orgs, and members, sitting in the same database as Payload's own collections for folders, chats, messages, usage logs, and RAG sources. Chat runs through the Vercel AI SDK's streaming text generation against OpenRouter, with usage and cost logging happening asynchronously once the stream finishes so bookkeeping never adds latency to the response.
Engineering Challenge
Routing every message through an LLM call just to decide which model should answer it would double the cost and latency of the cheap path. The original plan also called for a much richer governance layer — a rule-based PII/topic-blocklist classifier — which isn't built yet; what exists today is the model-routing/cost engine and a hard ban-enforcement path, with the richer content-policy classifier still a designed-but-unshipped feature.
Solution
A two-stage classifier: a heuristic pre-filter (message length, greeting patterns, code fences, list markers) resolves the obvious cases for free, and only ambiguous messages fall through to a structured-output LLM call that scores complexity and picks the model. Streaming responses and analytics writes are also decoupled — the chat stream finishes and persists messages on its own timeline, while cost/usage logging happens in a separate completion hook — so the two never compete for the same latency budget.