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:
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.
moose/ as a sibling workspace and configure dependencies + env vars.Events) in moose/src/index.ts.pnpm dev:moose.moose/, then call it from a Server Component or Server Action.This guide's examples mirror our reference project at examples/nextjs-moose:
./)moose/ workspace package that contains your ClickHouse models and query helpersIf 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.
Required for native module compatibility
Download →MooseStack uses Docker to run ClickHouse locally
Download →This section adds a moose/ workspace package next to your Next.js app and configures it for local development.
This guide assumes you're using pnpm workspaces. Your Next.js app package will depend on your local moose workspace package.
In the Next.js example, pnpm-workspace.yaml lives at the repo root and includes two packages: the Next.js app (.) and the Moose package (moose).
packages: - "." - "moose"If you want the quickest setup, you can pull the moose/ workspace package from our Next.js example and drop it next to your Next.js app (as ./moose):
pnpm dlx tiged 514-labs/moose/examples/nextjs-moose/moose mooseThen install dependencies and build the moose package:
pnpm install
pnpm -C moose run buildAdd moose as a workspace dependency in your Next.js app workspace package.json:
{ "dependencies": { "moose": "workspace:*" }}If your Next.js app is not at the repo root, add these configurations to your repo root package.json (not your Next.js app's package.json).
Add the required pnpm.onlyBuiltDependencies configuration to your repo root package.json. Optionally, you can also add convenience scripts to run Moose commands from the repo root:
{ "pnpm": { "onlyBuiltDependencies": [ "@confluentinc/kafka-javascript", "@514labs/kafka-javascript" ] }, "scripts": { "dev:moose": "pnpm -C moose run dev", "build:moose": "pnpm -C moose run build", "moose": "pnpm -C moose run moose" }}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;This step is required: the moose package initializes its ClickHouse client from environment variables, so these values must be present when your Next.js server starts.
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=falseNext.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.
Update your .gitignore to exclude MooseStack generated files:
# MooseStack generated files# ignore your moose workspace build output/moose/dist.moose/.ts-node/In this step you'll define your first ClickHouse model as an OlapTable object.
The starter moose package you copied into your repo already includes an example model at moose/src/models.ts:
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"],});Add more tables by exporting additional OlapTable objects from moose/src/models.ts (or create separate model files and re-export them). See OlapTable Reference.
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.
Leave this running in its own terminal for the rest of the guide:
# run this from your repo root
pnpm dev:mooseThe provided moose workspace is configured to automatically rebuild the package for use inside your Next.js app whenever you make changes to your models.
[http_server_config]on_reload_complete_script = "pnpm build"See Dev Environment Configuration for more details.
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();}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`SELECT * FROM ${Events} ORDER BY ${Events.columns.event_time} DESC LIMIT ${limit}`, );}Make sure your moose package exports the models and queries:
export * from "./models";export * from "./queries";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);}Do not call ClickHouse from the browser. Use Server Components, Route Handlers, or Server Actions so credentials remain server-side.
There's no separate production Moose Runtime to deploy. You just need to:
Make sure ddl_plan = true is set in [features] in moose/moose.config.toml:
[features]streaming_engine = falsedata_model_v2 = trueddl_plan = trueThis 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 moose generate migration \
--clickhouse-url "clickhouse://user:password@your-prod-host:8443/db?secure=true" \
--saveThis creates files in migrations/ including plan.yaml.
pnpm moose migrate \
--clickhouse-url "clickhouse://user:password@your-prod-host:8443/db?secure=true"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=trueIf you see import errors for import ... from "moose", confirm:
pnpm-workspace.yaml includes both your Next.js app and the moose workspacepnpm install so workspace:* links are createdpnpm -C moose run build (or your equivalent script) to generate moose/dist/Native module errors (NODE_MODULE_VERSION)
node_modules, run pnpm installUsing the App Router
Package manager (workspaces)