Duck-UIDuck-UI

Architecture

System design, package structure, and runtime flow of Duck-UI

Architecture

Package Structure

Duck-UI is a monorepo with four packages. @duck_ui/core sits at the bottom -- the React and Web Components packages both import from it. The CDN package bundles everything into a single script.

┌──────────────────────────────────────────────────┐
│                  @duck_ui/cdn                     │
│          Pre-bundled IIFE for <script> tag        │
│         (bundles core + elements + duckdb)        │
└──────────────────┬───────────────────────────────┘
                   │ bundles
         ┌─────────┴─────────┐
         │                   │
┌────────▼────────┐ ┌───────▼─────────┐
│ @duck_ui/embed  │ │ @duck_ui/elements│
│ React bindings  │ │ Web Components   │
│ (hooks, JSX)    │ │ (Custom Elements)│
└────────┬────────┘ └───────┬─────────┘
         │ imports           │ imports
         └────────┬──────────┘

         ┌────────▼────────┐
         │ @duck_ui/core   │
         │ Pure JS engine  │
         │ Chart factories │
         │ DuckDB manager  │
         │ Filter system   │
         └────────┬────────┘
                  │ peer dep
         ┌────────▼────────┐
         │ @duckdb/duckdb  │
         │     -wasm       │
         └─────────────────┘

Package Details

PackageContentsFrameworkPeer Deps
@duck_ui/coreDuckUI class, DuckDBManager, ConnectionPool, QueryExecutor, QueryCache, FilterInjector, chart factories, themesNone (pure JS)@duckdb/duckdb-wasm >=1.28.0
@duck_ui/embedDuckUIProvider, hooks (useDuckUI, useTheme, useQuery), Chart, DataTable, KPICard, FilterBar, Dashboard, ExportButtonReact 18+@duckdb/duckdb-wasm >=1.28.0
@duck_ui/elementsCustom Elements: duck-provider, duck-chart, duck-table, duck-kpi, duck-dashboard, duck-panel, duck-filter-bar, duck-export, individual filtersNone (Web Components)-- (bundles core)
@duck_ui/cdnSingle IIFE script, registers all Custom Elements, exposes window.DuckUINone-- (self-contained)

Source Layout

packages/
├── core/src/
│   ├── engine/         DuckDBManager, ConnectionPool, QueryExecutor, QueryCache
│   ├── filters/        FilterInjector, filter state
│   ├── charts/         toBarData, toLineData, toPieData, toSparklineData, etc.
│   ├── themes/         lightTheme, darkTheme, palettes
│   ├── DuckUI.ts       Imperative DuckUI class
│   └── index.ts        Public exports

├── embed/src/
│   ├── provider/       DuckUIProvider, context, hooks
│   ├── components/     Chart, DataTable, KPICard, FilterBar, ExportButton
│   ├── layout/         Dashboard, Dashboard.Panel
│   └── index.ts        Re-exports from core + React components

├── elements/src/
│   ├── elements/       DuckProvider, DuckChart, DuckTable, DuckKpi, etc.
│   ├── registry.ts     customElements.define() calls
│   └── index.ts        Auto-registers all elements on import

└── cdn/
    ├── src/index.ts    Imports elements + exposes DuckUI on window
    └── vite.config.ts  IIFE build config

Runtime Initialization Flow

React (@duck_ui/embed)

When <DuckUIProvider> mounts:

1. DuckDBManager.initialize()
   ├── Fetches WASM bundle info from jsDelivr
   ├── Selects best bundle for the browser
   ├── Creates a Web Worker
   └── Instantiates AsyncDuckDB

2. ConnectionPool(manager)
   └── Ready to hand out connections on acquire()

3. For each key in data prop:
   └── loadData(db, conn, data)
       ├── Array of objects → JSON string → registerFileBuffer → CREATE TABLE
       ├── { url }          → fetch → registerFileBuffer → CREATE TABLE
       ├── { url, format: 'parquet' } → registerFileURL → HTTP range reads
       ├── { fetch }        → call fn → JSON string → registerFileBuffer → CREATE TABLE
       └── File             → registerFileBuffer → CREATE TABLE

4. status = 'ready'
   └── Children render, hooks can execute queries

Web Components (@duck_ui/elements)

When <duck-provider> connects to the DOM:

1. connectedCallback()
   ├── Reads src, table, format attributes
   └── Creates internal DuckUI instance (from @duck_ui/core)

2. DuckUI.init({ [table]: { url: src, format } })
   └── Same engine flow as above

3. Dispatches 'duck-ready' CustomEvent
   └── Child elements query via closest <duck-provider>

Imperative (@duck_ui/core)

const ui = new DuckUI()
await ui.init({ orders: [...] })
// Engine is ready, call ui.query() directly

Query Execution Flow

When a component calls useQuery(sql) (React) or an element's sql attribute is set:

1. Check engine status === 'ready'

2. Build effective SQL:
   ├── If filters are active:
   │   └── FilterInjector.inject(sql, filters, tableName)
   │       → Wraps sql as subquery, adds WHERE clauses
   └── Otherwise: use sql as-is

3. Check QueryCache:
   ├── Cache hit → return cached result
   └── Cache miss → continue

4. QueryExecutor.execute(effectiveSql):
   ├── ConnectionPool.acquire() → get a DuckDB connection
   ├── conn.query(sql) → run SQL in WASM Worker
   ├── Coerce values (BigInt → Number, Date → ISO string, etc.)
   ├── ConnectionPool.release(conn)
   └── Return QueryResult { rows, columns, rowCount, executionTime }

5. Cache the result

6. Return result to component/element/caller

Filter Flow

User interacts with filter (React component or Custom Element)
  → setFilter('region', 'North')
    → filterVersion increments
      → All queries re-run (they depend on filterVersion)
        → FilterInjector wraps SQL:
            Original: SELECT * FROM sales
            Injected: SELECT * FROM (SELECT * FROM sales) AS _filtered
                      WHERE "region" = 'North'
          → Components/elements re-render with filtered data

For Web Components, filter changes also dispatch a duck-filter-change CustomEvent on the provider element.

Filter conditions by value type:

FilterValueSQL Generated
'North'"col" = 'North'
42"col" = 42
true"col" = true
['A', 'B']"col" IN ('A', 'B')
{ min: 10, max: 100 }"col" >= 10 AND "col" <= 100
{ start: '2024-01-01', end: '2024-12-31' }"col" BETWEEN '2024-01-01' AND '2024-12-31'
null(skipped)

SQL-Level Pagination (DataTable / duck-table)

Both the React DataTable and the <duck-table> Custom Element use SQL-level pagination. Two queries run per page:

Base SQL: SELECT * FROM sales WHERE region = 'North'

Count query (cached):
  SELECT COUNT(*) AS _total FROM (SELECT * FROM sales WHERE region = 'North') AS _count_base

Page query:
  SELECT * FROM (SELECT * FROM sales WHERE region = 'North') AS _page_base
  ORDER BY revenue DESC
  LIMIT 25 OFFSET 50

Both queries run in parallel. Only the current page of rows is loaded into JavaScript.

Memory Model

┌─────────────────────────┐
│  Main Thread            │
│  ├── React / Custom El. │
│  ├── DuckUI instance    │
│  └── QueryResults (JS)  │
└─────────┬───────────────┘
          │ postMessage
┌─────────▼───────────────┐
│  Web Worker             │
│  └── DuckDB-WASM        │
│      ├── Tables (WASM)  │
│      ├── Query Engine   │
│      └── Memory Limit   │
└─────────────────────────┘
  • DuckDB runs entirely in a Web Worker (no main thread blocking)
  • Table data lives in WASM memory (default: 256 MB)
  • Query results are serialized and sent back to the main thread as JavaScript objects
  • The ConnectionPool manages up to 4 concurrent connections by default
  • The same engine powers all three integration modes (React, Web Components, imperative)

On this page