<p><strong>Building fast, friendly tables in Laravel — and why we let the database do the heavy lifting.</strong></p><p>There's a particular kind of dread that sets in when a ticket says "just add a table." You know how it goes. It starts innocent — a few columns, some sorting. Then someone wants filters. Then facets with counts. Then "can we group by region and show subtotals?" Then export. Then it's ten million rows and the page takes nine seconds and everyone's sad.</p><p>We built Elyra DataGrid because we were tired of that arc. This is the warm, honest tour: why it works the way it does, and how you actually use it — with real snippets you can paste.</p><p>Grab a coffee. This won't take long.</p><h2>The one idea that changes everything</h2><p>Most grid libraries load your rows into memory and sort/filter/paginate them there. That's lovely for 200 rows and a disaster for 2 million.</p><p>Elyra DataGrid flips it: the database does the work. The grid describes what it wants as a small, well-defined request, and your database — the thing that has indexes and has spent decades getting good at exactly this — answers it.</p><pre><code class="language-text">Browser  ──GridRequest──▶  Server (allow-list + SQL)  ──▶  Database
        ◀──GridResponse──                             ◀──
</code></pre><p>One shared protocol (GridRequest / GridResponse), described once and understood by both the TypeScript clients and the PHP server. One headless engine. Three clients (Livewire, Vue, Svelte) that all wrap the same brain. You learn it once.</p><p>And a quiet principle we're proud of: cost is opt-in. Total row counts, facet counts, aggregates — none of it runs unless a feature on screen actually needs it. Totals get cached per filter. Facets load lazily when a dropdown opens. Your default table is cheap.</p><h2>How: a grid in about a dozen lines</h2><p>Here's the server side. This object is the security boundary — only declared columns can ever be sorted, filtered, or written:</p><pre><code class="language-php">use Elyra\DataGrid\Server\Grid;

$grid = Grid::table('sales')-&gt;key('id')
    -&gt;columns(['sku', 'region', 'status'])
    -&gt;column('revenue', type: 'number')
    -&gt;searchable(['sku', 'region']);
</code></pre><p>Answering a request from your client is one line:</p><pre><code class="language-php">return $grid-&gt;respond(GridRequest::fromArray($request-&gt;all()));
</code></pre><p>That's it. Sorting, paging, and filtering are on by default. You didn't wire any of it.</p><h3>On Livewire, it's a component</h3><p>Extend DataGrid, describe your source and columns, flip a few flags:</p><pre><code class="language-php">class SalesGrid extends DataGrid
{
    public bool $selectable = true;
    public bool $groupable  = true;

    protected function definition(): GridDefinition
    {
        return Grid::table('sales')-&gt;key('id')
            -&gt;columns(['sku', 'region', 'status'])
            -&gt;column('revenue', type: 'number')
            -&gt;searchable(['sku'])
            -&gt;authorize(fn (string $ability) =&gt; auth()-&gt;check());
    }

    protected function columns(): array
    {
        return [
            ['field' =&gt; 'sku',     'header' =&gt; 'SKU', 'frozen' =&gt; true],
            ['field' =&gt; 'region',  'header' =&gt; 'Region'],
            ['field' =&gt; 'revenue', 'header' =&gt; 'Revenue', 'type' =&gt; 'number', 'align' =&gt; 'right'],
        ];
    }
}
</code></pre><pre><code class="language-blade">&lt;livewire:sales-grid /&gt;
</code></pre><p>Sortable headers, a search box, pagination, row selection, grouping — all there, all server-driven.</p><h3>On Vue or Svelte, it's a headless engine</h3><p>Same brain, no DOM opinions. You bring the fetch, you render however you like:</p><pre><code class="language-ts">import { createGrid } from "@elyra/datagrid-client-core";

const grid = createGrid({
  key: "id",
  columns: [
    { field: "name",   header: "Name" },
    { field: "city",   header: "City" },
    { field: "amount", header: "Amount", type: "number" },
  ],
  paging: { pageSize: 25 },
  search: { mode: "substring", fields: ["name", "city"] },
  fetch: (req) =&gt; api.post("/customers/grid", req).then((r) =&gt; r.data),
});
</code></pre><p><code>grid.getState()</code> gives you rows and metadata; <code>grid.subscribe(render)</code> re-renders on change; <code>grid.toggleSort("name")</code>, <code>grid.setSearch("berlin")</code>, <code>grid.goToPage(2)</code> do what they say. (The Vue and Svelte wrapper components are there too if you'd rather not render by hand.)</p><h2>Turn features on as you need them</h2><p>The core stays simple; the power is opt-in. A few favourites:</p><pre><code class="language-php">// Group by region with subtotals (ROLLUP under the hood)
public bool $groupable = true;

// Self-referencing tree data, children loaded lazily per node
public bool|array $tree = ['parentField' =&gt; 'parent_id', 'childLimit' =&gt; 500];

// Inline + batch editing, with a clipboard-paste that writes in one transaction
public bool $editable = true;
</code></pre><p>And when you want to feel like you're living in the future — ask the grid in plain language. The model's answer is re-validated against your allow-list and compiled through the same bound-parameter path as a hand-built filter, so it can only ever produce filters your grid already permits:</p><pre><code class="language-php">$filter = $grid-&gt;ask('paid orders over 5k in the nordics last quarter');
</code></pre><p>Off-target guess? It quietly drops the offending condition. Never unsafe SQL. Safe by design, not by hope.</p><h2>New in 0.4.0: bring your own PostgreSQL 🐘</h2><p>This is the part we're most excited to share. Elyra DataGrid is native to ElyraSQL, but it's genuinely portable — and as of 0.4.0 there's a first-class PostgreSQL driver:</p><pre><code class="language-php">Grid::table('users')-&gt;driver('pgsql')-&gt;connection('pgsql')
    -&gt;columns(['name', 'email', 'country'])
    -&gt;column('signups', type: 'number')
    -&gt;searchable(['name', 'email']);
</code></pre><p>It's not a lowest-common-denominator port. It speaks Postgres:</p><ul><li><p>Identifiers quoted the Postgres way (<code>"col"</code>)</p></li><li><p>Subtotals via <code>GROUP BY ROLLUP(…)</code></p></li><li><p>Real percentiles and medians with <code>percentile_cont(p) WITHIN GROUP (ORDER BY …)</code></p></li><li><p>Genuine full-text search — <code>search: { mode: 'fulltext' }</code> compiles to <code>to_tsvector(…) @@ plainto_tsquery(…)</code></p></li></ul><p>And it's not just unit-tested for shape; there's a live integration test that runs the generated SQL against a real PostgreSQL server, so we know it actually parses and behaves. PostgreSQL joins MySQL, ClickHouse, and SQLite / Elyra SQL Anywhere — pick the one your app already uses, and everything degrades gracefully per dialect.</p><h2>A word on safety, because it matters</h2><p>Also new in 0.4.0, and worth saying plainly: writes and exports are now secure-by-default. A grid won't <code>mutate()</code>, <code>mutateBatch()</code>, or <code>export()</code> until you've told it who's allowed:</p><pre><code class="language-php">Grid::table('sales')-&gt;key('id')
    -&gt;authorize(fn (string $ability) =&gt; auth()-&gt;user()?-&gt;can($ability, Sale::class) ?? false);
    // ...or -&gt;withoutAuthorization() when your route/policy already guards it
</code></pre><p>Forget to configure it and you get a loud exception before anything touches the database — not a silent open door. That's the kind of default we like: the safe path is the easy path.</p><h2>Come on in</h2><p>That's the whole pitch, really. Let the database do what it's good at. Share one protocol so your front end and back end never drift. Keep the common case cheap and the powerful case one flag away. And make the safe thing the default thing.</p><p>If you've been dreading "just add a table" — maybe you don't have to anymore.</p><pre><code class="language-bash">composer require elyra/datagrid-server
</code></pre><p>Happy gridding. ☕</p>