Data Apps
Build interactive React applications with client-side DuckDB WASM, Perspective.js, and a built-in component library inside Definite Docs
Data apps are source-authored React applications that compile to a single HTML file and run inside a Doc. They go beyond standard YAML dashboards by giving you full control: client-side filtering, interactive pivot tables, custom charts, and rich component library.
npx create-definite-app my-app — scaffold + build with one command
@definite-app/data-apps — the build tool, runtime, and component library
When to use data apps vs YAML dashboards
| YAML Dashboard | Data App |
|---|---|
| Standard charts, tables, KPIs | Custom interactive UI (expandable rows, conditional formatting) |
| Multiple tiles in a grid layout | Single full-screen experience |
| Cube/SQL datasets with built-in viz | Custom JavaScript visualizations (ECharts, Perspective.js) |
| Quick to build with Fi | Source-authored React with component library |
| No build step required | Compiled to single HTML via build tool |
Use data apps when you need: client-side filtering and drill-down, multi-tab layouts, interactive Perspective grids, ECharts visualizations, or fully custom UI.
Use YAML dashboards when: standard charts, tables, and KPIs are sufficient. They're faster to build, easier to maintain, and Fi can create them from natural language.
How data flows
app.json Definite platform Browser DuckDB WASM App.tsx
(manifest) --> (server-side fetch) --> (local tables) --> (client-side SQL)
- app.json declares every data resource the app needs. Each resource has a key, a
kind, and asource(SQL, Cube, or file). - The Definite platform reads the manifest and fetches data server-side from DuckLake, Cube, or GCS. The app never talks to the warehouse directly.
- The runtime loads the fetched data into a browser-side DuckDB WASM instance as local tables.
kind: "dataset"resources become DuckDB tables;kind: "json"resources are returned as plain arrays. - App.tsx queries those local tables via
useSqlQuery(dataset, sql, deps). These queries run in the browser against DuckDB WASM, not against the server.
Column names in your useSqlQuery SQL must match the aliases in your app.json SQL. If app.json has SELECT foo AS myColumn, the local table column is myColumn. Mismatches cause "Binder Error: Referenced column not found" at runtime.
Quick start
Scaffold a new data app from an app.json manifest with one command:
# Manifest-driven: write app.json first, then scaffold + generate App.tsx + build.
npx create-definite-app my-app --from ./app.json
# Or start from a blank template and edit:
npx create-definite-app my-app
The CLI is published as create-definite-app on npm. It depends on @definite-app/data-apps, which ships the build script (definite-build), runtime, components, and templates.
This produces:
my-app/
app.json # Manifest (declare your data resources here)
package.json # Pinned to @definite-app/data-apps
src/
main.tsx # Entry point (boilerplate)
App.tsx # Generated from your manifest if you passed --from
Step 1: Define your data in app.json
Declare the data resources your app needs:
{
"version": 2,
"name": "Sales Dashboard",
"entry": "src/main.tsx",
"resources": {
"sales": {
"kind": "dataset",
"source": {
"type": "sql",
"sql": "SELECT region AS region, STRFTIME(order_date, '%Y-%m-%d') AS orderDate, amount::DOUBLE AS amount FROM LAKE.PUBLIC.orders LIMIT 200000"
},
"public": false
},
"regions": {
"kind": "json",
"source": {
"type": "sql",
"sql": "SELECT region_id AS regionId, region_name AS regionName FROM LAKE.PUBLIC.regions ORDER BY 2"
},
"public": false
}
}
}
Always use type: "sql" with camelCase column aliases. This gives you full control over column names. Avoid type: "cube" because Cube responses use long title-based column names that are painful to work with.
Step 2: Build your UI in App.tsx
import React from "react";
import {
useDataset, useSqlQuery, useTheme,
AppShell, Card, KpiCard, LoadingState, ErrorState,
} from "@definite/runtime";
export default function App() {
const { theme, toggleTheme } = useTheme();
const data = useDataset("sales");
const kpis = useSqlQuery(
data,
data.tableRef
? `SELECT COUNT(*)::INTEGER AS orders, SUM(amount)::INTEGER AS revenue FROM ${data.tableRef}`
: "",
[],
);
if (data.loading) return <LoadingState message="Loading sales data..." />;
if (data.error) return <ErrorState title="Load Error" message={data.error} />;
return (
<AppShell title="Sales Dashboard" theme={theme} onToggleTheme={toggleTheme}>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<KpiCard title="Total Orders" value={kpis.data?.[0]?.orders} format="number" loading={kpis.loading} />
<KpiCard title="Revenue" value={kpis.data?.[0]?.revenue} format="currency" loading={kpis.loading} />
</div>
</AppShell>
);
}
Step 3: Build
cd my-app
npm install
npm run build # invokes the bundled `definite-build` bin
This produces my-app/dist/index.html (and dist/index.embedded.html for multi-tenant embedded use). The build validates that all useDataset() and useJsonResource() calls reference keys that exist in app.json.
Step 4: Deploy
Upload dist/index.html to Definite Drive (via the MCP server or UI) and create a Doc with a full-screen HTML tile:
version: 1
schemaVersion: "2025-01"
kind: dashboard
metadata:
name: "Sales Dashboard"
datasets: {}
layout:
columns: 36
tiles:
- id: app
x: 0
y: 0
w: 36
h: 22
type: html
fullScreen: true
driveFile: "apps-v2/my-app/dist/index.html"
Manifest resources
Resource kinds
| Kind | Hook | Use for |
|---|---|---|
dataset | useDataset(key) | Data loaded into browser DuckDB WASM as a queryable table |
json | useJsonResource(key) | Small lookup lists returned as plain arrays (dropdowns, config) |
Source types
| Type | Description |
|---|---|
sql | SQL executed server-side against DuckLake. Recommended. |
duckdbFile | A .duckdb file downloaded from Drive/GCS and attached locally |
cube | Cube semantic model query. Not recommended for data apps. |
Public embeds
For publicly shared Docs, resources need a snapshot block:
"snapshot": {
"format": "json",
"drivePath": "apps-v2/my-app/snapshots/data.json"
}
Public mode uses cached/snapshotted data only. No live query execution.
Runtime hooks
The runtime library provides React hooks for data loading and querying:
| Hook | Returns | Purpose |
|---|---|---|
useDataset(key) | DatasetHandle | Load dataset into browser DuckDB, get tableRef for SQL |
useSqlQuery(dataset, sql, deps) | QueryState<T> | Run client-side SQL against loaded dataset |
useJsonResource(key) | QueryState<T> | Load JSON resource as array |
useTheme() | { theme, toggleTheme } | Dark/light mode |
usePerspective(dataset) | { client, perspectiveTable } | Initialize Perspective viewer for a dataset |
All hooks that load data (useDataset, useJsonResource) cache results in IndexedDB with a 24-hour TTL. Call refresh() on the returned handle for a hard refresh.
UI components
The runtime includes a full component library. To see exported components, hooks, and types, grep the runtime source after npm install:
grep -nE '^export (const|function|type|interface|class) ' \
node_modules/@definite-app/data-apps/runtime/definite-runtime.tsx
| Category | Components |
|---|---|
| Layout | AppShell, Card, TabGroup |
| Data display | KpiCard, DataTable, ReportTable, Badge |
| Charts | EChart, PerspectivePanel |
| Inputs | Select, MultiSelect, FilterPills, TextInput, DateInput |
| Feedback | LoadingState, ErrorState, Tooltip, ResourceCacheBadge |
Best practices
- Always use SQL resources (
type: "sql") over Cube resources for data apps. You control column names via aliases. - Alias columns to camelCase in
app.jsonSQL. Convert dates withSTRFTIME(col, '%Y-%m-%d'). - Keep client-side SQL simple. Use
app.jsonSQL for joins, CASE WHEN, and complex filters. UseuseSqlQueryonly for GROUP BY, SUM of pre-computed columns, and date range filters. - Cast SUM results to
::INTEGERin client-side SQL. DuckDB WASM may return HUGEINT which JavaScript can't handle cleanly. - Pre-compute conditional flags in
app.jsonSQL. DuckDB WASM has known issues with compoundCASE WHENexpressions (they silently return 0).
Caching
The runtime caches data loads in IndexedDB with a 24-hour TTL. Cache keys include the resource key, mode, and manifest definition, so rebuilt apps invalidate automatically.
Use ResourceCacheBadge in your AppShell meta slot to show cache status and provide a "Clear cache & reload" button.
Next steps
The CLI scaffolder. Generates App.tsx from a manifest in one shot.
The framework: build tool, runtime, components, examples.
Programmatically create data apps via the MCP server or AI agents
Configuration options for all tile types including HTML tiles