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). (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
A secured SvGrid Studio screen. SvAuthGate requires a signed-in user before it shows the grid, presenting a login and sign-up form and a signed-in bar with sign out. The demo uses a mock auth client so any email and password works with no backend; in a real app you pass your supabase-js client through createSupabaseAuth, and Row-Level Security scopes each user to their own rows: auth establishes who, RLS enforces what.
Data-app Studio · auth - a secured screen. SvAuthGate requires a signed-in user before it shows the app; sign out returns to the login form.
This demo uses a *mock* auth client (any email + password signs you in) so it runs with no backend. In a real app you pass your supabase-js client - createClient(url, anonKey) - and Row-Level Security scopes each user to their own rows. Auth establishes WHO; RLS enforces WHAT they can see.
Imports, features and API used
Imports: @svgrid/grid, @svgrid/enterprise, ../shared/SvGridStudio.svelte
Frequently asked questions
What does SvAuthGate render?
A login form with a sign-up switch while there is no session, and its children plus a signed-in bar once there is one. Sign out clears the session and returns to the form.
How do I connect Supabase Auth?
Create your client with createClient(url, anonKey) and pass it through createSupabaseAuth as the gate's client. Sessions persist in the browser, so a reload keeps the user signed in.
Does signing in restrict the data?
Not by itself. Authentication identifies the user; Row-Level Security policies on the table use that identity to decide which rows the data source can read and write.
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.
- 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.
- SvGrid vs svelte-headless-table - A practical comparison of SvGrid and svelte-headless-table covering reactivity model, rendering approach, feature scope, and when each is the right choice for a Svelte 5 project.
Source code (196-studio-auth.svelte)
<script lang="ts">
/**
* Data-app Studio · auth - a secured screen. `SvAuthGate` requires a signed-in
* user before it shows the app; sign out returns to the login form.
*
* This demo uses a *mock* auth client (any email + password signs you in) so it
* runs with no backend. In a real app you pass your `supabase-js` client -
* `createClient(url, anonKey)` - and Row-Level Security scopes each user to
* their own rows. Auth establishes WHO; RLS enforces WHAT they can see.
*/
import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'
import {
SvAuthGate,
SvGridEditPanel,
createInMemoryDataSource,
schemaToColumns,
type EntitySchema,
type SupabaseAuthClientLike,
} from '@svgrid/enterprise'
import SvGridStudio from '../shared/SvGridStudio.svelte'
type Customer = { id: string; name: string; email: string; tier: string; mrr: number; active: boolean }
const schema: EntitySchema<Customer> = {
name: 'customers', label: 'Customer', idField: 'id',
fields: [
{ field: 'id', type: 'text', primaryKey: true, readonly: true, hidden: { form: 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' },
],
}
// A mock supabase-js-shaped auth client: any credentials sign in. Swap for
// `createClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)` in a real app.
function mockAuthClient(): SupabaseAuthClientLike {
let cb: ((event: string, session: { user: { id: string; email?: string } } | null) => void) | undefined
let session: { user: { id: string; email?: string } } | null = null
const emit = () => cb?.('x', session)
const signIn = async ({ email }: { email: string }) => {
session = { user: { id: 'demo-user', email } }
emit()
return { error: null }
}
return {
auth: {
getSession: async () => ({ data: { session } }),
onAuthStateChange: (fn) => { cb = fn; return { data: { subscription: { unsubscribe() {} } } } },
signInWithPassword: signIn,
signUp: signIn,
signOut: async () => { session = null; emit(); return { error: null } },
},
}
}
const client = mockAuthClient()
const seed: Customer[] = [
{ id: 'c1', name: 'Ada Lovelace', email: '[email protected]', tier: 'enterprise', mrr: 1200, active: true },
{ id: 'c2', name: 'Alan Turing', email: '[email protected]', tier: 'pro', mrr: 240, active: true },
{ id: 'c3', name: 'Grace Hopper', email: '[email protected]', tier: 'enterprise', mrr: 980, active: true },
{ id: 'c4', name: 'Linus Torvalds', email: '[email protected]', tier: 'pro', mrr: 360, active: true },
]
const columns = schemaToColumns(schema)
const source = createInMemoryDataSource(seed, schema)
let view = $state<ServerState<Customer>>({
rows: [], total: 0, loading: false, saving: false, error: null,
pageIndex: 0, pageSize: 10, pageCount: 1, sortModel: [], filterModel: {},
})
const controller = createServerDataSource(source, {
pageSize: 10, optimistic: true, getRowId: (r) => r.id, onChange: (s) => (view = s),
})
controller.refresh()
let editing = $state<Customer | null | undefined>(undefined)
let genId = 5
async function save({ mode, id, values }: { mode: 'create' | 'edit'; id: string | null; values: Partial<Customer> }) {
if (mode === 'create') { await controller.createRow({ id: `c${genId++}`, ...values }); controller.setPage(view.pageCount - 1) }
else if (id) { await controller.updateRow(id, values) }
editing = undefined
}
</script>
<SvGridStudio
title="Secured"
subtitle="The grid is behind <code>SvAuthGate</code>. Sign in with <em>any</em> email + password (mock auth), then Sign out."
>
<SvAuthGate {client} title="Sign in to Studio">
<div class="st__toolbar">
<button class="st-btn st-btn--primary" onclick={() => (editing = null)}>+ New customer</button>
<span class="st-hint">You are signed in - in a real app, RLS would scope these rows to you.</span>
</div>
<SvGrid responsive={true}
columnResize
data={view.rows}
{columns}
loading={view.loading}
fitColumns
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)}
showPagination
externalPagination
rowCount={view.total}
pageIndex={view.pageIndex}
pageSize={view.pageSize}
onPaginationChange={({ pageIndex, pageSize }) => (pageSize !== view.pageSize ? controller.setPageSize(pageSize) : controller.setPage(pageIndex))}
containerHeight={300}
/>
{#if editing !== undefined}
<SvGridEditPanel {schema} row={editing} presentation="modal" persistKey="studio" onSubmit={save} onCancel={() => (editing = undefined)} />
{/if}
</SvAuthGate>
</SvGridStudio>More Studio examples
- 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.
- 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 · 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.