Embed ClickHouse analytics in your Next.js app
Overview
This quickstart shows how to embed MooseStack into an existing Next.js (App Router) app so you can define ClickHouse schema in TypeScript and serve type-safe analytics from Server Components and Server Actions—while keeping Next.js as your only deployed backend.
From this you will be able to:
- Model ClickHouse schema in TypeScript, alongside your Next.js code
- Integrate type-safe ClickHouse queries into your Server Components and Server Actions
- Generate/apply migrations against production ClickHouse when you're ready to deploy
What runs in production?
Only your Next.js app runs in production. MooseStack is used as a library + CLI to manage schema/migrations and to generate an importable package (moose/dist) that your Next.js runtime uses to query ClickHouse.
Guide outline
- Get started: add
moose/as a sibling workspace and configure dependencies + env vars. - Model table: define a table model (for example,
Events) inmoose/app/models.ts. - Start local ClickHouse: run
moose dev. - Use in Next.js: define a client + query helper in
moose/, then call it from a Server Component or Server Action. - Deploy to production: generate/apply migrations and set production env vars.
Project structure
This guide's examples mirror our reference project at examples/nextjs-moose:
- The Next.js app is at the repo root (
./) - A sibling
moose/workspace package that contains your ClickHouse models and query helpers
If you're applying this to an existing repo with a different layout, keep the package name moose the same and adjust only the paths/commands.
Prerequisites
Before you start
Node.js 20+
Required for native module compatibility
Download →Docker Desktop
MooseStack uses Docker to run ClickHouse locally
Download →Existing Next.js 13+ app
Using the App Router
pnpm
Package manager (workspaces)
Get started
This section creates a moose/ workspace package next to your Next.js app using moose init.
Initialize Moose in your monorepo
From your monorepo, cd to the directory where you want the Moose project folder to be created (commonly your repo root so this becomes ./moose), then run:
cd <path-in-your-monorepo>
moose init moose typescript-emptyThen install dependencies from your monorepo root:
cd <your-monorepo-root>
pnpm installUse generated config as-is
moose init creates moose/package.json and moose/tsconfig.json with the required compiler output settings. You do not need extra manual compilation wiring.
Add your moose workspace as a dependency
Add moose as a workspace dependency in your Next.js app workspace package.json:
{ "dependencies": { "moose": "workspace:*" }}Configure Next.js (server bundling)
Update next.config.ts to avoid bundling server-only dependencies:
import type { NextConfig } from "next"; const nextConfig: NextConfig = { serverExternalPackages: [ "@514labs/moose-lib", "@confluentinc/kafka-javascript", "@514labs/kafka-javascript", ],}; export default nextConfig;Configure environment variables (Next.js runtime)
Create or update your Next.js app's .env.local file with the following ClickHouse connection details for your local development environment:
MOOSE_CLIENT_ONLY=true
MOOSE_CLICKHOUSE_CONFIG__DB_NAME=local
MOOSE_CLICKHOUSE_CONFIG__HOST=localhost
MOOSE_CLICKHOUSE_CONFIG__PORT=18123
MOOSE_CLICKHOUSE_CONFIG__USER=panda
MOOSE_CLICKHOUSE_CONFIG__PASSWORD=pandapass
MOOSE_CLICKHOUSE_CONFIG__USE_SSL=falseConfiguring environment variables in Next.js
Next.js automatically loads environment variables from .env.local (and other .env* files) at runtime. These variables are available in process.env when your Server Components and Server Actions execute.
Model table
In this step you'll define your first ClickHouse model as an OlapTable object.
Create a new file at moose/app/models.ts and add your first table definition:
import { OlapTable } from "@514labs/moose-lib"; export interface EventModel { id: string; amount: number; event_time: Date; status: 'completed' | 'active' | 'inactive';} export const Events = new OlapTable<EventModel>("events", { orderByFields: ["event_time"],});This defines a TypeScript interface for your table schema and creates an OlapTable that maps to a ClickHouse table named events. When you run moose dev in the next step, Moose will automatically create this table in your local ClickHouse instance.
Adding more tables
Add more tables by exporting additional OlapTable objects from moose/app/models.ts (or create separate model files and re-export them). See OlapTable Reference.
Start local ClickHouse
Start the Moose Runtime in dev mode. This brings up a local ClickHouse instance (via Docker) and hot-reloads schema changes to it whenever you edit your models.
Navigate to your moose directory and run:
moose devmoose dev handles TypeScript compilation automatically and reloads after successful incremental compiles.
See Dev Environment Configuration for more details.
Use in Next.js
1) Set up a shared ClickHouse client in moose/
First, create a shared ClickHouse client initializer inside your moose package. This keeps connection logic in one place and lets Next.js Server Components/Actions call simple query helpers.
import { getMooseClients, Sql, QueryClient } from "@514labs/moose-lib"; async function getClickhouseClient(): Promise<QueryClient> { const { client } = await getMooseClients({ host: process.env.MOOSE_CLICKHOUSE_CONFIG__HOST ?? "localhost", port: process.env.MOOSE_CLICKHOUSE_CONFIG__PORT ?? "18123", username: process.env.MOOSE_CLICKHOUSE_CONFIG__USER ?? "panda", password: process.env.MOOSE_CLICKHOUSE_CONFIG__PASSWORD ?? "pandapass", database: process.env.MOOSE_CLICKHOUSE_CONFIG__DB_NAME ?? "local", useSSL: (process.env.MOOSE_CLICKHOUSE_CONFIG__USE_SSL ?? "false") === "true", }); return client.query;} export async function executeQuery<T>(query: Sql): Promise<T[]> { const queryClient = await getClickhouseClient(); const result = await queryClient.execute(query); return result.json();}2) Define a query helper in moose/app/queries.ts
Next, define a query helper that uses the shared client and export it from "moose". The example defines a very basic query for you to use as a starting point:
import { sql } from "@514labs/moose-lib";import { Events, EventModel } from "./models";import { executeQuery } from "./client"; export async function getEvents(limit: number = 10): Promise<EventModel[]> { return await executeQuery<EventModel>( sql.statement`SELECT * FROM ${Events} ORDER BY ${Events.columns.event_time} DESC LIMIT ${limit}`, );}3) Export from moose/app/index.ts
Make sure your moose package exports the models and queries:
export * from "./models";export * from "./queries";4) Import and use the helper in Next.js
Now your Next.js app can import and use the helper from "moose" in any Server Component or Server Action, like this:
Server Component example:
import { getEvents } from "moose"; export default async function AnalyticsPage() { const events = await getEvents(10); return ( <div> <h1>Recent Events</h1> <ul> {events.map((event) => ( <li key={event.id}> {event.id}: {event.amount} ({event.status}) </li> ))} </ul> </div> );}Server Action example:
"use server"; import { getEvents } from "moose"; export async function getRecentEvents(limit: number = 10) { return await getEvents(limit);}Server-only: keep ClickHouse access on the server
Do not call ClickHouse from the browser. Use Server Components, Route Handlers, or Server Actions so credentials remain server-side.
Deploy to production
There's no separate production Moose Runtime to deploy. You just need to:
- Apply your schema to production ClickHouse
- Configure your production environment with production credentials
Enable planned migrations
Make sure ddl_plan = true is set in [features] in moose/moose.config.toml:
[features]ddl_plan = trueGenerate a migration plan
Important: Use production credentials
This command connects to the ClickHouse instance you specify in --clickhouse-url and generates a migration plan for that database. Use your production ClickHouse URL + credentials if you intend to deploy these schema changes to production.
pnpm -C moose run moose generate migration \
--clickhouse-url "clickhouse://user:password@your-prod-host:8443/db?secure=true" \
--saveThis creates files in migrations/ including plan.yaml.
Apply the migration
pnpm -C moose run moose migrate \
--clickhouse-url "clickhouse://user:password@your-prod-host:8443/db?secure=true"Configure production environment
In production, set these environment variables in your deployment platform (Vercel, Railway, etc.). Make sure to use your production ClickHouse URL + credentials.
MOOSE_CLIENT_ONLY=true
MOOSE_CLICKHOUSE_CONFIG__DB_NAME=production_db
MOOSE_CLICKHOUSE_CONFIG__HOST=your-clickhouse-host.example.com
MOOSE_CLICKHOUSE_CONFIG__PORT=8443
MOOSE_CLICKHOUSE_CONFIG__USER=prod_user
MOOSE_CLICKHOUSE_CONFIG__PASSWORD=prod_password
MOOSE_CLICKHOUSE_CONFIG__USE_SSL=trueTroubleshooting
If you see import errors for import ... from "moose", confirm:
- Your root
pnpm-workspace.yamlincludes both your Next.js app and themooseworkspace - You ran
pnpm installsoworkspace:*links are created moose devis running and TypeScript compilation completed successfully- If your
moosepackage uses a customoutDir, verifymoose/package.json(main,types,exports) points to that output
Native module errors (NODE_MODULE_VERSION)
- Ensure Node.js v20+, delete
node_modules, runpnpm install
Next steps
- OlapTable Reference — Primary keys, engines, and configuration
- Read Data — Query patterns and the Moose client
- Migrations — Schema versioning and migration strategies
- Schema Optimization — Ordering keys and partitioning