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
| Package | Contents | Framework | Peer Deps |
|---|---|---|---|
@duck_ui/core | DuckUI class, DuckDBManager, ConnectionPool, QueryExecutor, QueryCache, FilterInjector, chart factories, themes | None (pure JS) | @duckdb/duckdb-wasm >=1.28.0 |
@duck_ui/embed | DuckUIProvider, hooks (useDuckUI, useTheme, useQuery), Chart, DataTable, KPICard, FilterBar, Dashboard, ExportButton | React 18+ | @duckdb/duckdb-wasm >=1.28.0 |
@duck_ui/elements | Custom Elements: duck-provider, duck-chart, duck-table, duck-kpi, duck-dashboard, duck-panel, duck-filter-bar, duck-export, individual filters | None (Web Components) | -- (bundles core) |
@duck_ui/cdn | Single IIFE script, registers all Custom Elements, exposes window.DuckUI | None | -- (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 configRuntime 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 queriesWeb 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() directlyQuery 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/callerFilter 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 dataFor Web Components, filter changes also dispatch a duck-filter-change CustomEvent on the provider element.
Filter conditions by value type:
| FilterValue | SQL 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 50Both 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
ConnectionPoolmanages up to 4 concurrent connections by default - The same engine powers all three integration modes (React, Web Components, imperative)