Tinybird turns ClickHouse into instant analytics APIs — ingest via POST https://api.tinybird.co/v0/events?name=datasource with JSON newline-delimited rows. Each row ingested within milliseconds is queryable seconds later. Pipes are SQL transformation chains: a Pipe can have multiple transformation nodes ending in an API endpoint with TOKEN and parameter definitions. Published endpoints: GET https://api.tinybird.co/v0/pipes/my_pipe.json?token=TOKEN&startDate=2024-01-01 — response is { data: [...], statistics: { elapsed, rows_read, bytes_read } }. Query parameters in Pipe SQL: {% if defined(userId) %} WHERE user_id = {{ String(userId) }} {% end %}. Materialized views flush aggregates in real time: a Pipe with TYPE materialized writes results to a landing datasource. Kafka connector: tinybird connector create kafka --connection-url ... streams events without a custom ingest pipeline. CLI workflow: tb init, tb push deploys datasources and pipes, tb dev hot-reloads, tb token ls manages tokens. Node.js: plain fetch to pipe endpoints — no SDK needed. Claude Code generates Tinybird analytics backends, real-time event pipelines, and SaaS usage dashboards.
CLAUDE.md for Tinybird
## Tinybird Stack
- No SDK needed — all HTTP with fetch
- Ingest: POST https://api.tinybird.co/v0/events?name=TABLE with NDJSON body (each row is a JSON line)
- Query pipe: GET https://api.tinybird.co/v0/pipes/PIPE_NAME.json?token=TOKEN¶m=value
- Auth: Authorization: Bearer TOKEN header on all requests
- Response: { data: [...], statistics: { elapsed, rows_read, bytes_read } }
- Region base URLs: us-east: api.us-east.tinybird.co, EU: api.tinybird.co
- Pipe params: {% if defined(date) %} WHERE date >= {{ Date(date) }} {% end %} (Jinja2)
Tinybird TypeScript Client
// lib/tinybird/client.ts — type-safe Tinybird HTTP client
const TINYBIRD_HOST = process.env.TINYBIRD_HOST ?? "https://api.tinybird.co"
const TINYBIRD_TOKEN = process.env.TINYBIRD_TOKEN!
type TinybirdResponse<T> = {
data: T[]
statistics: {
elapsed: number
rows_read: number
bytes_read: number
}
error?: string
}
/** Send events to a Tinybird datasource via the Events API */
export async function ingestEvents<T extends Record<string, unknown>>(
datasource: string,
events: T[],
): Promise<void> {
const ndjson = events.map((e) => JSON.stringify(e)).join("\n")
const res = await fetch(`${TINYBIRD_HOST}/v0/events?name=${datasource}`, {
method: "POST",
headers: {
Authorization: `Bearer ${TINYBIRD_TOKEN}`,
"Content-Type": "application/x-ndjson",
},
body: ndjson,
})
if (!res.ok) {
const text = await res.text()
throw new Error(`Tinybird ingest error ${res.status}: ${text}`)
}
}
/** Query a published Pipe endpoint with typed parameters */
export async function queryPipe<T>(
pipeName: string,
params: Record<string, string | number | boolean> = {},
token = TINYBIRD_TOKEN,
): Promise<TinybirdResponse<T>> {
const qs = new URLSearchParams(
Object.entries(params)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => [k, String(v)]),
)
const url = `${TINYBIRD_HOST}/v0/pipes/${pipeName}.json?token=${token}&${qs}`
const res = await fetch(url, { next: { revalidate: 60 } }) // ISR-friendly
if (!res.ok) {
const text = await res.text()
throw new Error(`Tinybird pipe error ${res.status}: ${text}`)
}
return res.json() as Promise<TinybirdResponse<T>>
}
// ── Typed query helpers ────────────────────────────────────────────────────
export type PageViewEvent = {
timestamp: string // ISO 8601
session_id: string
user_id?: string
path: string
referrer?: string
country?: string
browser?: string
device?: "desktop" | "mobile" | "tablet"
}
export type ConversionEvent = {
timestamp: string
user_id: string
event_name: string
value?: number
currency?: string
properties?: string // JSON string
}
export async function trackPageView(event: Omit<PageViewEvent, "timestamp">): Promise<void> {
await ingestEvents("page_views", [{ ...event, timestamp: new Date().toISOString() }])
}
export async function trackConversion(event: Omit<ConversionEvent, "timestamp">): Promise<void> {
await ingestEvents("conversions", [{ ...event, timestamp: new Date().toISOString() }])
}
// ── Analytics query results ────────────────────────────────────────────────
export type DailyStats = {
date: string
pageviews: number
unique_visitors: number
sessions: number
avg_session_duration: number
}
export type TopPage = {
path: string
pageviews: number
unique_visitors: number
bounce_rate: number
}
export type GeoBucket = {
country: string
country_code: string
pageviews: number
percentage: number
}
export type FunnelStep = {
step: string
users: number
drop_off: number
conversion_rate: number
}
export async function getDailyStats(
startDate: string,
endDate: string,
token?: string,
): Promise<DailyStats[]> {
const { data } = await queryPipe<DailyStats>("daily_stats", { startDate, endDate }, token)
return data
}
export async function getTopPages(
startDate: string,
limit = 20,
token?: string,
): Promise<TopPage[]> {
const { data } = await queryPipe<TopPage>("top_pages", { startDate, limit }, token)
return data
}
export async function getGeoDistribution(
startDate: string,
endDate: string,
token?: string,
): Promise<GeoBucket[]> {
const { data } = await queryPipe<GeoBucket>("geo_distribution", { startDate, endDate }, token)
return data
}
Tinybird Datasource Definitions
-- datasources/page_views.datasource
SCHEMA >
`timestamp` DateTime,
`session_id` String,
`user_id` String DEFAULT '',
`path` String,
`referrer` String DEFAULT '',
`country` String DEFAULT '',
`browser` String DEFAULT '',
`device` String DEFAULT ''
ENGINE "MergeTree"
ENGINE_PARTITION_KEY "toYYYYMM(timestamp)"
ENGINE_SORTING_KEY "timestamp, session_id, path"
TTL "timestamp + INTERVAL 90 DAY"
-- pipes/daily_stats.pipe
NODE daily_agg
SQL >
%
SELECT
toDate(timestamp) AS date,
count() AS pageviews,
uniq(session_id) AS sessions,
uniq(user_id) AS unique_visitors,
avg(session_duration) AS avg_session_duration
FROM page_views
WHERE
timestamp >= {{ DateTime(startDate, '2024-01-01') }}
AND timestamp <= {{ DateTime(endDate, '2024-12-31') }}
GROUP BY date
ORDER BY date ASC
TYPE endpoint
-- pipes/top_pages.pipe
NODE top_pages_node
SQL >
%
SELECT
path,
count() AS pageviews,
uniq(session_id) AS unique_visitors,
countIf(is_bounce = 1) / count() AS bounce_rate
FROM page_views
WHERE
timestamp >= {{ DateTime(startDate, '2024-01-01') }}
AND path NOT IN ('/favicon.ico', '/robots.txt')
GROUP BY path
ORDER BY pageviews DESC
LIMIT {{ Int32(limit, 20) }}
TYPE endpoint
Next.js Analytics Dashboard API
// app/api/analytics/route.ts — serve Tinybird data to the dashboard
import { NextResponse } from "next/server"
import { getDailyStats, getTopPages, getGeoDistribution } from "@/lib/tinybird/client"
import { auth } from "@/lib/auth"
export async function GET(req: Request) {
const session = await auth()
if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
const url = new URL(req.url)
const startDate = url.searchParams.get("startDate") ?? "2024-01-01"
const endDate = url.searchParams.get("endDate") ?? new Date().toISOString().slice(0, 10)
const [daily, pages, geo] = await Promise.all([
getDailyStats(startDate, endDate),
getTopPages(startDate, 10),
getGeoDistribution(startDate, endDate),
])
return NextResponse.json({ daily, pages, geo })
}
// app/api/track/route.ts — edge-compatible event ingestion
import { trackPageView } from "@/lib/tinybird/client"
export const runtime = "edge"
export async function POST(req: Request) {
const body = await req.json()
await trackPageView({
session_id: body.sessionId,
user_id: body.userId ?? "",
path: body.path,
referrer: body.referrer ?? "",
country: req.headers.get("cf-ipcountry") ?? "",
browser: body.browser ?? "",
device: body.device ?? "desktop",
})
return new Response(null, { status: 202 })
}
For the ClickHouse Cloud alternative when needing a self-managed or cloud ClickHouse instance with full SQL access, shard-level control, custom compression codecs, or running ClickHouse on your own infrastructure — ClickHouse Cloud is the underlying engine while Tinybird adds a CI/CD workflow, version-controlled Pipes, and publishable REST API endpoints on top of it without managing servers, see the ClickHouse guide. For the Axiom alternative when primarily shipping application observability (logs, traces, spans) with OpenTelemetry integration and a query UI resembling Splunk/Datadog rather than a customer-facing analytics API — Axiom is log/observability-focused while Tinybird specializes in building analytics APIs that product teams expose to end-users, see the Axiom guide. The Claude Skills 360 bundle includes Tinybird skill sets covering real-time event ingestion, Pipe SQL, and analytics API patterns. Start with the free tier to try real-time analytics generation.