Architecture
Layers of the Better Tables monorepo and how data moves from schema to UI.
Better Tables keeps database concerns, table contracts, and React rendering in separate packages so you can swap adapters and own your UI source.
Package map
| Package | Role |
|---|---|
@better-tables/core | Column builders, filter/sort/pagination managers, adapter contract types. No React, no DB driver. |
@better-tables/adapters-drizzle | Schema introspection, JOIN/filter/pagination query generation for Postgres/MySQL/SQLite. |
@better-tables/adapters-toolkit | ORM-agnostic machinery for adapter authors. |
@better-tables/ui | React table/filter components (shadcn-style). Distributed via CLI copy, not npm. |
@better-tables/cli | better-tables init copies UI into a consumer project. |
Data flow
defineTable() columns
↓
betterTables({ database: adapter })
↓
adapter.fetchData / getFacetedValues / …
↓
@better-tables/ui (copied) renders + manages client state
↓
optional URL sync (filters, sort, page)Flagship API surface
Prefer these entry points everywhere:
betterTables({ database, defaults? })— factory forfetchData,define, cell-edit actionsdefineTable<typeof tables>()('name', (t) => ({ columns }))— RSC-safe table definitionst.text/t.number/t.date/t.option/ … — path builders with fluent modifierstables.fetchData(table, params)— server/query boundary
For tables without a database — playgrounds, tests, client-only arrays — memoryAdapter(rows) runs the same pipeline in memory, and defineTableRow<Row>() defines a table from hand-built columns. See Custom adapters and Columns.
Plugins (data hooks)
betterTables({ plugins: [...] }) takes plugins — instance-level data hooks that wrap the query path. Each plugin can implement beforeFetch (rewrite the params before the adapter sees them) and/or afterFetch (transform the result). They run in array order around tables.fetchData; a throwing hook fails the fetch (nothing is swallowed), and the no-plugin path is unaffected.
import { betterTables, logPlugin } from '@better-tables/core';
const tables = betterTables({
database: adapter,
plugins: [
logPlugin({ onFetch: ({ table, rowCount, total }) => track(table, rowCount, total) }),
{
name: 'only-active',
// AND `activeFilter` onto whatever filters are present — preserving an
// existing group (a bare `[]` fallback would silently drop an OR tree).
beforeFetch: ({ params }) => ({
...params,
filters: {
kind: 'group',
logic: 'and',
children: [
...(Array.isArray(params.filters)
? params.filters
: params.filters
? [params.filters]
: []),
activeFilter,
],
},
}),
},
],
});Plugins vs. modules. Plugins are npm-distributed data hooks on betterTables({ plugins }) (this section). Modules are copied UI source added with better-tables add — see UI & CLI. Different tiers, kept distinct.
Authoritative demos
Shipped examples live under apps/marketing — especially src/lib/demo/support/ and the routes under /examples. Those use the same 0.6 API this handbook teaches.