Cross-posted from my blog. The original, with any updates, lives on fabwaseem.com.
Repo: github.com/fabwaseem/forgekit, MIT licensed. Clone it, run one setup script, and you have a working AI chat app on your own machine.
A Next.js AI starter kit is a ready-to-run Next.js project that already wires up the hard parts of an AI app: streaming chat, model providers, auth, a database, retrieval over your own documents and a deploy path. Forgekit is my open-source Next.js AI starter kit. It is built on Next.js 16, the AI SDK, Better Auth, Drizzle and PostgreSQL with pgvector, and it runs on Docker, Coolify or Vercel.
This post explains what Forgekit includes, how it is put together, how it compares with other options, and how to run, extend and deploy it. Every number here comes from building and running the repo myself.
A Next.js AI starter kit is a template repository that gives you a working AI application on day one instead of a blank create-next-app. A good one includes streaming chat UI, a server route that calls a model, saved conversations, authentication, a database, and a clear way to add providers and deploy. You start from working code and change it, rather than gluing libraries together yourself.
The value is in the boring parts. Streaming a reply is easy. Saving the conversation correctly, limiting abuse, scoping documents to a user, tracking token usage and shipping a container that migrates its own database take most of the time.
Forgekit includes a streaming chat with saved threads, a model picker across five providers, three built-in tools, document upload with retrieval-augmented generation (RAG) on pgvector, email and GitHub sign-in, per-user rate limiting, token usage by model, and an MCP endpoint that exposes your knowledge base to other AI clients. It ships with Docker, tests and CI.
| Feature | What you get | Built with |
|---|---|---|
| Streaming chat | Saved threads, message parts, stop button | AI SDK UI ( useChat ),streamText |
| Model picker | OpenAI, Anthropic, Gemini, xAI, Ollama. A provider shows only when its key or URL is set | @ai-sdk/* ,ollama-ai-provider-v2 |
| Tools | Weather (Open-Meteo), public page fetch, calculator, each rendered as a card | AI SDK tool() with Zod schemas |
| RAG | PDF, Markdown and text uploads, chunked, embedded and searched with numbered citations | pgvector, HNSW cosine index, Drizzle |
| Auth | Email and password, plus GitHub OAuth when its env vars are set | Better Auth |
| Limits and usage | Per-user sliding window, token totals per model | Upstash Redis if set, Postgres otherwise |
| MCP | POST /api/mcp with asearch_knowledge tool behind anfk_ bearer key |
MCP TypeScript SDK |
| Ops | Health check, migrations on boot, standalone output | Docker, output: 'standalone' |
| Quality | Unit tests, Playwright smoke test, ESLint, GitHub Actions | Vitest, Playwright |
Pinned versions at the time of writing: Next.js 16.4.2, React 19.3, ai 7.0.142, Better Auth 1.7.10, Drizzle ORM 0.45.4 and the MCP SDK 1.32.1.
To run Forgekit locally, install Node.js 22, pnpm and Docker, then clone the repo and run the setup script. It copies .env.example to .env, generates an auth secret, starts PostgreSQL with pgvector through Docker Compose, installs dependencies and applies the Drizzle migrations. Then pnpm dev serves the app on port 3000.
git clone https://github.com/fabwaseem/forgekit.git
cd forgekit
bash scripts/setup.sh
pnpm dev
Open http://localhost:3000, create an account, and add at least one provider to .env. The cheapest way to try it is a local model:
OLLAMA_BASE_URL=http://127.0.0.1:11434
OLLAMA_MODELS=llama3.2,qwen2.5
OPENAI_API_KEY=sk-...
The model picker only lists providers you have configured, so you never see a model that would fail.
On a clean checkout I ran pnpm install --frozen-lockfile, pnpm typecheck, pnpm lint, pnpm test (6 files, 15 tests) and pnpm build, and all passed. I then ran the app against a pgvector Postgres and a stubbed Ollama endpoint. Sign-up, chat creation, a streamed reply, saved history, usage totals and a document upload all worked, both with pnpm start and in the Docker image.
That smoke test found two bugs, which are now fixed on main. Assistant messages were saved with an empty id, so the second reply in a thread failed to save. And pnpm db:migrate and pnpm start did not read .env. That is what testing a starter is for.
Forgekit's architecture is a single Next.js App Router app and a single PostgreSQL database. The browser streams messages to one route handler, /api/chat. It checks the session and rate limit, then calls streamText with the chosen model and tools. Tool calls, including retrieval over pgvector, run on the server. When the stream ends, the messages and token usage are written to Postgres.
Keeping everything in Postgres is a deliberate choice. Chats, messages, documents, vector chunks, API keys, usage and the rate-limit fallback all live in one database you already know how to back up. pgvector is an extension, not another service.
The chat route loads the saved thread, appends the new user message, streams the model's reply and saves the result when the stream ends. Here is the core of app/api/chat/route.ts:
const result = streamText({
model: resolved.model,
system: SYSTEM_PROMPT,
messages: await convertToModelMessages(messages),
tools,
stopWhen: isStepCount(6),
onEnd: async ({ totalUsage }) => {
await recordUsage({
userId: user.id,
modelId: body.modelId!,
inputTokens: totalUsage.inputTokens,
outputTokens: totalUsage.outputTokens,
totalTokens: totalUsage.totalTokens,
});
},
});
return createUIMessageStreamResponse({
stream: toUIMessageStream({
stream: result.stream,
originalMessages: messages,
sendSources: true,
generateMessageId: () => crypto.randomUUID(),
onEnd: async ({ messages: nextMessages, isAborted }) => {
if (isAborted) return;
await saveMessages(chat.id, nextMessages, body.modelId!);
},
}),
});
stopWhen: isStepCount(6) caps how many tool-call rounds a single answer can take, so a looping model cannot run up your bill. generateMessageId is the fix mentioned above. Without it, assistant messages have no stable id to persist. The AI SDK's message persistence guide covers the same pattern.
RAG in Forgekit works in two steps. On upload, the text is split into 800-character chunks with 120 characters of overlap, embedded into 1536-dimension vectors and stored in a pgvector column with an HNSW cosine index. At question time, the searchKnowledge tool embeds the query, finds the closest chunks for that user only and returns them as numbered citations.
The search itself is a short Drizzle query using cosineDistance, following Drizzle's vector similarity guide:
const embedding = await embedQuery(query);
const similarity = sql<number>`1 - (${cosineDistance(chunk.embedding, embedding)})`;
const rows = await db
.select({ content: chunk.content, documentName: document.name, score: similarity })
.from(chunk)
.innerJoin(document, eq(document.id, chunk.documentId))
.where(eq(chunk.userId, userId))
.orderBy(desc(similarity))
.limit(Math.min(Math.max(limit, 1), 8));
Two details matter. First, the where(eq(chunk.userId, userId)) filter means one user can never retrieve another user's documents. Second, the index is HNSW with vector_cosine_ops, the approximate index type recommended in the pgvector README for fast search without a training step.
Embeddings use the first available of OpenAI (text-embedding-3-small), Google ( gemini-embedding-001, with outputDimensionality set to 1536) or Ollama, unless EMBEDDING_PROVIDER picks one. The column is fixed at 1536 dimensions. For fully local RAG, pick an Ollama embedding model that outputs 1536 dimensions, or change the column and its migration together. The common nomic-embed-text model outputs 768.
To add a provider to Forgekit, install its AI SDK package, add its models to the catalog in lib/ai/models.ts with the env var that enables them, and add one case to languageModelFor in lib/ai/providers.ts. The model picker, the /api/models endpoint and the chat route pick it up automatically, because they all read the same catalog.
Here is the whole provider switch:
export function languageModelFor(option: ModelOption) {
switch (option.provider) {
case "openai":
return openai(option.model);
case "anthropic":
return anthropic(option.model);
case "google":
return google(option.model);
case "xai":
return xai(option.model);
case "ollama":
return createOllama({
baseURL: ollamaBaseURL(process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434"),
}).chat(option.model);
}
}
For example, to add Mistral you would run pnpm add @ai-sdk/mistral, add "mistral" to PROVIDERS, add { provider: "mistral", model: "mistral-large-latest", label: "Mistral Large", env: "MISTRAL_API_KEY" } to CATALOG, and add case "mistral": return mistral(option.model);. TypeScript then flags any place that does not handle the new provider. The AI SDK docs list every official provider package.
A tool is a description, a Zod input schema and an execute function, defined with the AI SDK's tool() helper inside createChatTools in lib/ai/tools.ts. The model decides when to call it, the server runs it, and the result is streamed back as a message part that the UI can render as a card.
weather: tool({
description: "Get the current weather for a city or place using a public forecast.",
inputSchema: z.object({
location: z.string().min(1).describe("City or place name"),
}),
execute: async ({ location }) => {
// geocode with Open-Meteo, then fetch current conditions
},
}),
Tools receive the userId, so anything they read or write can be scoped to the signed-in user. The built-in webFetch tool also refuses private network addresses, which you should keep in mind for any tool that fetches URLs a model chose.
Forgekit's MCP endpoint lets other AI clients, such as Claude Desktop, Cursor or your own agent, search your Forgekit knowledge base. POST /api/mcp speaks the Model Context Protocol and exposes one tool, search_knowledge. Callers authenticate with a personal fk_ bearer key created on the Usage page, and only the hash of each key is stored.
If you want to build one of these from scratch, I wrote a full guide on how to build an MCP server in Next.js.
Forgekit's closest alternative is Vercel's open-source Chatbot template, a full-featured, hackable Next.js AI chatbot that is a great choice on Vercel. Forgekit differs mainly in being self-hosting first: direct provider SDKs plus local Ollama, retrieval in your own Postgres, and one Docker image. Both are good starting points. The right one depends on where you deploy.
| Forgekit | Vercel Chatbot template | Build from scratch | |
|---|---|---|---|
| Model access | Direct provider SDKs (5) plus local Ollama | Many providers through Vercel AI Gateway | Whatever you wire up |
| Database | Your PostgreSQL with pgvector | Neon serverless Postgres | Your choice |
| RAG over your documents | Built in, pgvector with citations | Not the template's focus | You build it |
| Auth | Better Auth (email, GitHub) | Auth.js | You build it |
| File storage | Text extracted into Postgres | Vercel Blob | Your choice |
| Best fit | Docker, Coolify or any VPS, plus Vercel | Vercel with its managed services | Full control, most work |
| License | MIT | See the repo | Yours |
Comparison based on each project's README as of October 2026. Check the repos for current details.
If you are all-in on Vercel's platform, the Vercel template integrates its services seamlessly. If you want to own the whole stack on your own server, or run models locally, Forgekit is designed for that.
To deploy Forgekit, build the included multi-stage Dockerfile, run it next to a PostgreSQL server that has the vector extension, and set DATABASE_URL, BETTER_AUTH_SECRET, BETTER_AUTH_URL, your provider keys and RUN_MIGRATIONS=true. The container applies the Drizzle migrations on boot and serves Next.js standalone output on port 3000, with a /api/health check that pings the database.
docker compose up -d --build # app + pgvector/pgvector:pg17
curl http://localhost:3000/api/health # {"ok":true}
The production image I built measured 284 MB. It runs as a non-root user and contains only the standalone output, static assets and the migration files. I explain each layer in my production Next.js Dockerfile guide.
On Coolify, create a PostgreSQL resource using the pgvector/pgvector image, add the repo as a Dockerfile application, paste the env vars and deploy. My step-by-step Coolify guide covers domains, TLS and webhooks. One gotcha: inside a container, 127.0.0.1 is the container itself, so point OLLAMA_BASE_URL at a reachable Ollama host.
On Vercel, use a hosted Postgres with pgvector, run pnpm db:migrate against it once, and use hosted providers, since Ollama on your laptop is not reachable from Vercel. Not sure which to choose? See Coolify vs Vercel for Next.js.
Forgekit suits developers who want to ship an AI product on infrastructure they control. That includes indie hackers building an AI SaaS, teams that need internal chat over company documents, and anyone who wants to try local models before paying for an API. It is also a readable reference if you just want to see how streaming, tools, RAG and auth fit together in Next.js 16.
The lib folder is under 1,000 lines of TypeScript, so you can read the whole core in an afternoon. More of my work is on the projects page.
Get the code: github.com/fabwaseem/forgekit. Star it if it helps, and open an issue if something breaks.
Related: Forgekit uses Better Auth and Drizzle. For a tested Better Auth setup on Next.js 16, see Better Auth with Next.js 16, and for why Drizzle, see my Drizzle vs Prisma benchmark.
Related: wondering whether pgvector is enough or you need a dedicated vector database? I benchmarked pgvector against Qdrant on 100,000 OpenAI embeddings and compared Pinecone's limits in pgvector vs Pinecone vs Qdrant.
Related: upgrading your own app from AI SDK 6? My AI SDK 7 migration guide shows what the codemod misses: it fixed 15 lines but only 1 of 9 failing tests, and OpenTelemetry went silent until I added @ai-sdk/otel.
Yes. Forgekit is MIT licensed, so you can use, modify and sell products built on it. Keep the license notice in your copy of the code.
Yes for chat. Set OLLAMA_BASE_URL and run models locally with Ollama. Local RAG needs an Ollama embedding model that outputs 1536 dimensions, or a change to the vector column size.
No. Embeddings are stored in PostgreSQL with the pgvector extension and an HNSW cosine index. Your chats, users and vectors live in one database.
Forgekit uses Next.js 16 with the App Router, React 19 and AI SDK 7. Exact versions are pinned in package.json and listed in the README.
Yes. Use a hosted Postgres that supports pgvector, run the migrations once, and configure hosted model providers. Ollama is only practical on Docker or a VPS.
Forgekit has a per-user sliding-window rate limit (30 requests per 60 seconds by default, set with RATE_LIMIT_REQUESTS and RATE_LIMIT_WINDOW_SECONDS). It also records token usage per model, so you can add hard quotas on top.
Originally published on fabwaseem.com.