Skip to main content
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.

Scaffold a new app

npx create-definite-app my-app — scaffold + build with one command

Framework on npm

@definite-app/data-apps — the build tool, runtime, and component library

When to use data apps vs YAML dashboards

YAML DashboardData App
Standard charts, tables, KPIsCustom interactive UI (expandable rows, conditional formatting)
Multiple tiles in a grid layoutSingle full-screen experience
Cube/SQL datasets with built-in vizCustom JavaScript visualizations (ECharts, Perspective.js)
Quick to build with FiSource-authored React with component library
No build step requiredCompiled 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)
  1. app.json declares every data resource the app needs. Each resource has a key, a kind, and a source (SQL, Cube, or file).
  2. The Definite platform reads the manifest and fetches data server-side from DuckLake, Cube, or GCS. The app never talks to the warehouse directly.
  3. 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.
  4. 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

KindHookUse for
datasetuseDataset(key)Data loaded into browser DuckDB WASM as a queryable table
jsonuseJsonResource(key)Small lookup lists returned as plain arrays (dropdowns, config)

Source types

TypeDescription
sqlSQL executed server-side against DuckLake. Recommended.
duckdbFileA .duckdb file downloaded from Drive/GCS and attached locally
cubeCube 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:
HookReturnsPurpose
useDataset(key)DatasetHandleLoad 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
CategoryComponents
LayoutAppShell, Card, TabGroup
Data displayKpiCard, DataTable, ReportTable, Badge
ChartsEChart, PerspectivePanel
InputsSelect, MultiSelect, FilterPills, TextInput, DateInput
FeedbackLoadingState, ErrorState, Tooltip, ResourceCacheBadge

Best practices

  1. Always use SQL resources (type: "sql") over Cube resources for data apps. You control column names via aliases.
  2. Alias columns to camelCase in app.json SQL. Convert dates with STRFTIME(col, '%Y-%m-%d').
  3. Keep client-side SQL simple. Use app.json SQL for joins, CASE WHEN, and complex filters. Use useSqlQuery only for GROUP BY, SUM of pre-computed columns, and date range filters.
  4. Cast SUM results to ::INTEGER in client-side SQL. DuckDB WASM may return HUGEINT which JavaScript can’t handle cleanly.
  5. Pre-compute conditional flags in app.json SQL. DuckDB WASM has known issues with compound CASE WHEN expressions (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

create-definite-app

The CLI scaffolder. Generates App.tsx from a manifest in one shot.

@definite-app/data-apps

The framework: build tool, runtime, components, examples.

Agent Reference

Programmatically create data apps via the MCP server or AI agents

Tile Types Reference

Configuration options for all tile types including HTML tiles