SvGrid Studio · live SQL
The Studio stack backed by a REAL Postgres running in the browser via PGlite (WASM), no server. createSqlDataSource turns the grid's sort / filter / page requests into parameterized SQL run through PGlite; the executed query is shown live under the toolbar. Full CRUD with optimistic updates against actual Postgres. (requires @svgrid/enterprise)
A live, editable Svelte 5 data grid example from the SvGrid gallery (Studio). See the SvGrid documentation for the full API.
About this example
The SvGrid Studio data-app stack backed by a real Postgres running in the browser through PGlite, with no server. createSqlDataSource turns the grid's sort, filter and page requests into parameterized SQL executed through PGlite's query(text, params), and the statement is shown live under the toolbar as you interact. Full CRUD with optimistic updates runs against the actual database, with SvGridEditPanel rendering the edit form from the entity schema.
Data-app Studio (live SQL) - the same Studio stack, but backed by a REAL Postgres running entirely in your browser via PGlite (WASM). No server.
createSqlDataSource turns the grid's sort / filter / page requests into parameterized SQL and runs them through PGlite's query(text, params). Every action below executes actual Postgres - watch the SQL under the toolbar update as you sort, filter, and page.
Imports, features and API used
Imports: @electric-sql/pglite, @svgrid/grid, @svgrid/enterprise, ../shared/SvGridStudio.svelte
Frequently asked questions
How does the grid talk to Postgres?
createSqlDataSource implements the server data source contract over a query function. Sorting, filtering and paging become ORDER BY, WHERE and LIMIT OFFSET with bound parameters, and inserts, updates and deletes run as SQL too; PGlite executes them in WebAssembly.
Is PGlite required to use the SQL data source?
No. It only needs a function that runs SQL with parameters and returns rows, so a fetch to a server endpoint that proxies your database works the same way. PGlite makes the demo self-contained.
How are edits shown before the SQL completes?
The controller applies the change to the view immediately and reconciles with the query result, so a save feels instant and a failure rolls the row back with a message.
Related documentation
Related articles
- SvGrid Studio Tips and Tricks: Build Svelte Data Apps, Fast - Practical SvGrid Studio tips - AI app generation, a designer that emits real SvelteKit, schema-driven edit forms, code-behind with ctx.grid, production auth/data/deploy toggles, and real data-source binding - each with a snippet.
- Real-Time Grids - Live WebSocket Updates in SvGrid - How to wire a high-frequency WebSocket feed into SvGrid without frame drops - covering stable row identity, RAF batching, and flash feedback done right.
- SvGrid vs TanStack Table - A Deep Dive - A concrete architectural comparison of SvGrid and TanStack Table's Svelte adapter - how each handles reactivity, rendering, and feature composition, with code that shows exactly where they diverge.
Source code (193-studio-live-sql.svelte)
<script lang="ts">
/**
* Data-app Studio (live SQL) - the same Studio stack, but backed by a REAL
* Postgres running entirely in your browser via PGlite (WASM). No server.
*
* `createSqlDataSource` turns the grid's sort / filter / page requests into
* parameterized SQL and runs them through PGlite's `query(text, params)`.
* Every action below executes actual Postgres - watch the SQL under the
* toolbar update as you sort, filter, and page.
*/
import { PGlite } from '@electric-sql/pglite'
import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'
import {
SvGridEditPanel,
createSqlDataSource,
schemaToColumns,
type EntitySchema,
} from '@svgrid/enterprise'
import SvGridStudio from '../shared/SvGridStudio.svelte'
type Customer = {
id: number
name: string
email: string
tier: string
mrr: number
active: boolean
}
const schema: EntitySchema<Customer> = {
name: 'customers',
label: 'Customer',
idField: 'id',
fields: [
{ field: 'id', type: 'number', primaryKey: true, readonly: true },
{ field: 'name', type: 'text', required: true, minLength: 2 },
{ field: 'email', type: 'text', label: 'Email', required: true, format: 'email' },
{
field: 'tier',
type: 'enum',
options: [
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro' },
{ value: 'enterprise', label: 'Enterprise' },
],
},
{ field: 'mrr', type: 'number', label: 'MRR ($)', min: 0 },
{ field: 'active', type: 'boolean' },
],
}
const columns = schemaToColumns(schema)
let ready = $state(false)
let lastSql = $state('')
let editing = $state<Customer | null | undefined>(undefined)
let selected = $state<Customer[]>([])
let view = $state<ServerState<Customer>>({
rows: [], total: 0, loading: false, saving: false, error: null,
pageIndex: 0, pageSize: 10, pageCount: 1, sortModel: [], filterModel: {},
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let controller: any
const SEED = [
['Ada Lovelace', '[email protected]', 'enterprise', 1200, true],
['Alan Turing', '[email protected]', 'pro', 240, true],
['Grace Hopper', '[email protected]', 'enterprise', 980, true],
['Edsger Dijkstra', '[email protected]', 'free', 0, false],
['Barbara Liskov', '[email protected]', 'pro', 300, true],
['Donald Knuth', '[email protected]', 'enterprise', 1500, true],
['Margaret Hamilton', '[email protected]', 'pro', 420, true],
['Tim Berners-Lee', '[email protected]', 'enterprise', 1100, true],
['Linus Torvalds', '[email protected]', 'pro', 360, true],
['Katherine Johnson', '[email protected]', 'enterprise', 890, true],
['Dennis Ritchie', '[email protected]', 'pro', 275, false],
['Radia Perlman', '[email protected]', 'enterprise', 760, true],
['Ken Thompson', '[email protected]', 'pro', 310, true],
['Frances Allen', '[email protected]', 'free', 0, false],
['Vint Cerf', '[email protected]', 'enterprise', 1320, true],
['Guido van Rossum', '[email protected]', 'pro', 340, true],
['Bjarne Stroustrup', '[email protected]', 'enterprise', 720, true],
['Anita Borg', '[email protected]', 'pro', 410, true],
['John McCarthy', '[email protected]', 'free', 0, false],
['Shafi Goldwasser', '[email protected]', 'enterprise', 1180, true],
] as const
async function init() {
const db = new PGlite() // in-memory Postgres (WASM)
await db.exec(`
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
tier TEXT,
mrr INTEGER,
active BOOLEAN
);
`)
for (const [name, email, tier, mrr, active] of SEED) {
await db.query('INSERT INTO customers (name, email, tier, mrr, active) VALUES ($1, $2, $3, $4, $5)', [
name, email, tier, mrr, active,
])
}
const source = createSqlDataSource<Customer>({
schema,
table: 'customers',
dialect: { placeholders: '$', ilike: true },
execute: async (text, params) => {
// Show the interesting query (the SELECT / INSERT / UPDATE / DELETE),
// not the COUNT(*) that follows each read.
if (!text.startsWith('SELECT COUNT(')) lastSql = text
const res = await db.query(text, params)
return res.rows as Record<string, unknown>[]
},
})
controller = createServerDataSource<Customer>(source, {
pageSize: 10,
optimistic: true,
getRowId: (r) => String(r.id),
onChange: (s) => (view = s),
})
controller.refresh()
ready = true
}
init()
async function save({ mode, id, values }: { mode: 'create' | 'edit'; id: string | null; values: Partial<Customer> }) {
if (mode === 'create') {
await controller.createRow(values)
controller.setPage(view.pageCount - 1) // jump to the new row (appended at the end)
} else if (id) {
await controller.updateRow(id, values)
}
editing = undefined
}
async function removeSelected() {
for (const row of selected) await controller.deleteRow(String(row.id))
selected = []
}
</script>
<SvGridStudio
title="Live SQL"
subtitle="A real Postgres running in your browser (<code>PGlite</code>, WASM). Every action is actual SQL."
>
{#snippet actions()}
<span class="st-badge st-badge--live">● Postgres · WASM · no server</span>
{/snippet}
{#if !ready}
<div class="live__loading">Booting Postgres in the browser…</div>
{:else}
<div class="st__toolbar">
<button class="st-btn st-btn--primary" onclick={() => (editing = null)}>+ New customer</button>
<button class="st-btn" disabled={selected.length === 0} onclick={removeSelected}>
Delete{selected.length ? ` (${selected.length})` : ''}
</button>
</div>
<div class="live__sql"><span>SQL</span> <code>{lastSql}</code></div>
<SvGrid responsive={true}
columnResize
data={view.rows}
{columns}
loading={view.loading}
fitColumns
showRowSelection
sortable
externalSort
onSortingChange={(s) => controller.setSort(s)}
filterable
filterMode="row"
externalFilter
onFiltersChange={(f) =>
controller.setFilter({
global: f.global || undefined,
columns: Object.fromEntries(
f.columns.map((c) => [c.id, { operator: c.operator, value: c.value, valueTo: c.valueTo, selectedValues: c.selectedValues }]),
),
})}
onRowDoubleClick={(e) => (editing = e.row)}
onRowSelectionChange={(_sel, rows) => (selected = rows)}
showPagination
externalPagination
rowCount={view.total}
pageIndex={view.pageIndex}
pageSize={view.pageSize}
onPaginationChange={({ pageIndex, pageSize }) => {
if (pageSize !== view.pageSize) controller.setPageSize(pageSize)
else controller.setPage(pageIndex)
}}
containerHeight={340}
/>
{#if editing !== undefined}
<SvGridEditPanel {schema} row={editing} presentation="modal" persistKey="studio" onSubmit={save} onCancel={() => (editing = undefined)} />
{/if}
{/if}
</SvGridStudio>
<style>
.live__loading { padding: 40px; text-align: center; color: var(--sg-muted, #888); }
.live__sql { display: flex; align-items: center; gap: 8px; font-size: 12px; overflow: auto; }
.live__sql span { flex: 0 0 auto; font-weight: 700; color: var(--sg-muted, #888); }
.live__sql code { white-space: nowrap; color: var(--sg-fg, inherit); background: var(--sg-header-bg, #f6f6f6); padding: 4px 8px; border-radius: 6px; }
</style>More Studio examples
- Northwind on PGlite - The classic Northwind sample DB (categories, customers, products, orders, order_details) seeded into a real in-browser Postgres (PGlite, WASM). Switch between a five-table JOIN exposed as a read-only VIEW and the editable base tables - each via createSqlDataSource. Sort / filter / page / edit all run as parameterized SQL; new rows get an auto id from a Postgres IDENTITY sequence.
- SvGrid Studio · Supabase - The Studio stack over hosted Postgres on Supabase, straight from the browser via supabase-js (PostgREST) and your project's public anon key. createSupabaseDataSource maps the grid's sort / filter / page / CRUD onto the query builder, introspectSupabaseTable adapts to any table AND detects foreign keys (a FK column auto-becomes a searchable lookup in the form), and createSupabaseRealtime makes it LIVE - change a row in the Supabase dashboard and it flashes in the grid (toggle the Live pill). Paste your URL + anon key, run the one-time setup SQL, done. RLS keeps the anon key safe.
- SvGrid Studio · relations - Foreign-key lookup fields end to end. Contacts belong to a Company: the contact form renders a searchable Company picker (SvLookupInput + createRelationLookup) that queries the Companies source and stores the id, while the grid shows the resolved company NAME. The lookup runs over the same ServerDataSource contract, so related options can come from Supabase / REST / SQL / in-memory (here both entities are in-memory).
- SvGrid Studio · secured - A secured screen: SvAuthGate requires a signed-in user (Supabase Auth via createSupabaseAuth) before showing the Studio grid, with a login / sign-up form and a signed-in bar. Uses a mock auth client so any email + password works here; swap for your supabase-js client and Row-Level Security scopes each user to their own rows (auth = who, RLS = what).
- SvGrid Studio · chart - A live chart panel beside the grid, driven by the same data source: rowsToChartSpec aggregates the rows (group by tier / active, reduce MRR by sum / avg / count) and SvGridChart renders bar / pie / line - no external chart library. The chart doubles as a filter control: click a bar or slice to filter the grid to that category. Create / edit / delete and the chart updates.