Lightdash is open-source BI built on top of dbt — metrics live in schema.yml alongside your dbt models. meta: { label: "Orders", joins: ["users"] } in the dbt model’s YAML config. Dimensions: - name: status, meta: { label: "Status", type: string }. Metrics: - name: revenue, meta: { label: "Revenue", type: number, sql: "SUM(${TABLE}.amount_usd)", format: { type: currency } }, - name: order_count, meta: { label: "Orders", type: count }. Custom dimensions: sql: "DATE_TRUNC('week', ${TABLE}.created_at)", type: date. Filters on metrics: meta: { sql: "SUM(CASE WHEN ${TABLE}.status = 'completed' THEN ${TABLE}.amount_usd ELSE 0 END)" }. lightdash validate checks metric definitions against your dbt project. lightdash deploy syncs dbt project to Lightdash Cloud. Lightdash REST API: GET /api/v1/projects/{projectId}/charts lists charts, POST /api/v1/projects/{projectId}/explores/{exploreName}/runQuery runs a query. Chart embedding: GET /api/v1/embed/{embedId}/token returns a short-lived JWT, use as ?token=JWT query param on the embed URL. Self-hosted: docker run -p 8080:8080 --env-file .env lightdash/lightdash. Spaces: organize dashboards and charts into shared or private spaces. Scheduler: built-in Slack/email notifications when dashboard conditions are met. lightdash preview spins up a branch-scoped Lightdash instance for PR previews. Claude Code generates Lightdash metric YAML, REST API clients, embed integrations, and Docker deployment configurations.
CLAUDE.md for Lightdash
## Lightdash Stack
- Metrics defined in dbt schema.yml — meta.label, meta.type, meta.sql for columns
- Deploy: lightdash deploy --project myproject (Cloud) or lightdash preview (PR preview)
- REST API: /api/v1 — GET /projects, POST /explores/{name}/runQuery
- Auth: Bearer {LIGHTDASH_API_KEY} header — generate in Lightdash Settings → API Tokens
- Embed: /api/v1/embed/{embedId}/token → get JWT → iframe URL with ?token=JWT
- Self-hosted: docker compose up — lightdash + postgres + redis services
dbt Schema YAML with Lightdash Metrics
# models/marts/schema.yml — dbt schema with Lightdash metric definitions
version: 2
models:
- name: orders_daily
description: "Daily order records with user enrichment"
meta:
label: "Orders"
joins:
- join: users
sql_on: "${orders_daily}.user_id = ${users}.id"
columns:
- name: order_id
description: "Unique order identifier"
meta:
label: "Order ID"
type: string
primary_key: true
- name: user_id
description: "FK to users"
meta:
label: "User ID"
type: string
hidden: true
- name: amount_usd
description: "Order amount in USD"
meta:
label: "Amount (USD)"
type: number
format: { type: currency, currency: USD, round: 2 }
metrics:
- name: revenue
type: sum
label: "Total Revenue"
description: "Sum of all order amounts"
format: { type: currency, currency: USD }
- name: avg_order_value
type: average
label: "Average Order Value"
format: { type: currency, currency: USD, round: 2 }
- name: completed_revenue
type: sum
label: "Completed Revenue"
description: "Revenue from completed orders only"
filters:
- target: { fieldRef: status }
operator: equals
values: ["completed"]
- name: status
meta:
label: "Status"
type: string
- name: created_at
meta:
label: "Order Date"
type: timestamp
- name: created_date
meta:
label: "Order Date (Day)"
type: date
hidden: false
- name: user_plan
meta:
label: "User Plan"
type: string
- name: user_country
meta:
label: "User Country"
type: string
# Custom dimensions not in source data
meta:
additional_dimensions:
- name: order_size_bucket
label: "Order Size"
type: string
sql: >
CASE
WHEN ${amount_usd} < 25 THEN 'Small (<$25)'
WHEN ${amount_usd} < 100 THEN 'Medium ($25-$100)'
WHEN ${amount_usd} < 500 THEN 'Large ($100-$500)'
ELSE 'Enterprise (>$500)'
END
- name: days_to_first_order
label: "Days to First Order"
type: number
sql: "DATEDIFF('day', ${users.created_at}, ${orders_daily.created_at})"
- name: users
meta:
label: "Users"
columns:
- name: id
meta: { label: "User ID", type: string, primary_key: true }
- name: email
meta: { label: "Email", type: string }
- name: plan
meta: { label: "Plan", type: string }
- name: country
meta: { label: "Country", type: string }
- name: created_at
meta:
label: "Signup Date"
type: timestamp
metrics:
- name: new_users
type: count_distinct
label: "New Users"
description: "Distinct users by signup date"
Lightdash REST API Client
// lib/lightdash/client.ts — Lightdash REST API client
const LIGHTDASH_URL = process.env.LIGHTDASH_URL ?? "https://app.lightdash.cloud"
const LIGHTDASH_API_KEY = process.env.LIGHTDASH_API_KEY!
const LIGHTDASH_PROJECT = process.env.LIGHTDASH_PROJECT_ID!
async function lightdashFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
const res = await fetch(`${LIGHTDASH_URL}/api/v1${path}`, {
...options,
headers: {
"Content-Type": "application/json",
"Authorization": `ApiKey ${LIGHTDASH_API_KEY}`,
...((options.headers as Record<string, string>) ?? {}),
},
})
const body = await res.json()
if (!res.ok) throw new Error(`Lightdash API error ${res.status}: ${JSON.stringify(body)}`)
return body.results as T
}
export type ExploreQuery = {
exploreName: string
dimensions: string[] // e.g. ["orders_daily_status", "orders_daily_created_date_day"]
metrics: string[] // e.g. ["orders_daily_revenue", "orders_daily_order_count"]
filters?: Record<string, unknown>
limit?: number
sorts?: Array<{ fieldId: string; descending: boolean }>
}
export async function runExploreQuery(query: ExploreQuery): Promise<{
rows: Array<Record<string, { value: { raw: unknown; formatted: string } }>>
fields: Record<string, { label: string; type: string }>
}> {
return lightdashFetch(`/projects/${LIGHTDASH_PROJECT}/explores/${query.exploreName}/runQuery`, {
method: "POST",
body: JSON.stringify({
dimensions: query.dimensions,
metrics: query.metrics,
filters: query.filters ?? { dimensions: {}, metrics: {} },
limit: query.limit ?? 500,
sorts: query.sorts ?? [],
tableCalculations: [],
}),
})
}
export async function listDashboards(): Promise<Array<{
uuid: string
name: string
description: string
updatedAt: string
}>> {
return lightdashFetch(`/projects/${LIGHTDASH_PROJECT}/dashboards`)
}
export async function getEmbedToken(embedId: string): Promise<{ token: string; expiresIn: number }> {
return lightdashFetch(`/embed/${embedId}/token`, { method: "GET" })
}
Embed Integration (Next.js)
// app/api/lightdash-embed/route.ts — SSO embed token for users
import { NextResponse } from "next/server"
import { auth } from "@/lib/auth"
const LIGHTDASH_URL = process.env.LIGHTDASH_URL!
const LIGHTDASH_API_KEY = process.env.LIGHTDASH_API_KEY!
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 embedId = url.searchParams.get("embedId")
if (!embedId) return NextResponse.json({ error: "Missing embedId" }, { status: 400 })
// Fetch short-lived token from Lightdash
const tokenRes = await fetch(`${LIGHTDASH_URL}/api/v1/embed/${embedId}/token`, {
headers: { Authorization: `ApiKey ${LIGHTDASH_API_KEY}` },
})
const { results } = await tokenRes.json()
return NextResponse.json({ token: results.token, expiresIn: results.expiresIn })
}
// components/LightdashEmbed.tsx — iFrame embed component
"use client"
import { useEffect, useState } from "react"
export function LightdashEmbed({
embedId,
height = 600,
}: {
embedId: string
height?: number
}) {
const [embedUrl, setEmbedUrl] = useState<string | null>(null)
useEffect(() => {
fetch(`/api/lightdash-embed?embedId=${embedId}`)
.then((r) => r.json())
.then(({ token }) => {
const base = process.env.NEXT_PUBLIC_LIGHTDASH_URL
setEmbedUrl(`${base}/embed/${embedId}?token=${token}`)
})
}, [embedId])
if (!embedUrl) return <div className="animate-pulse bg-gray-100 rounded-lg" style={{ height }} />
return (
<iframe
src={embedUrl}
width="100%"
height={height}
className="rounded-xl border border-gray-200"
title="Analytics Dashboard"
/>
)
}
For the Metabase alternative when needing a BI tool designed for non-technical users who want to build their own charts without writing SQL or YAML — Metabase provides drag-and-drop chart builders and natural language questions while Lightdash is targeted at data teams who already use dbt and want metrics defined as code alongside their dbt models. For the Cube alternative when needing a standalone semantic layer that works without dbt — Cube has its own YAML/JavaScript data model language and can connect to any database without a dbt project while Lightdash is specifically designed to work on top of an existing dbt project and won’t add value without one. The Claude Skills 360 bundle includes Lightdash skill sets covering dbt metric YAML, REST API clients, and embed integrations. Start with the free tier to try open source BI generation.