Live dashboard (fullstack sync)
A SvelteKit dashboard where every value on screen is
server-authoritative: a keyed profile channel, a live task query that
re-runs when the filter changes, one optimistic mutate, and a slow
aggregate polled on a declared cadence. The client never fetches — it
declares, and the sync machinery replicates.
Project layout
Section titled “Project layout”dashboard/├── src/│ ├── lib/│ │ ├── models.js # the schemas (the spine)│ │ ├── db.server.js # server-only data access│ │ └── para-sync-manifest.js # GENERATED by para-kit emit│ ├── routes/│ │ ├── +page.pui # the dashboard│ │ ├── +page.pui.server-sources.pts # GENERATED (escape-analyzed)│ │ └── para-sync/+server.ts # written ONCE by para-kit emit│ └── hooks.client.js # transport wiring├── svelte.config.js # .pui setup — see /docs/pui/└── package.jsonThe schemas — src/lib/models.js
Section titled “The schemas — src/lib/models.js”export schema User { id: bigint, name: str(1..=60),}
export schema Task { id: bigint, title: str, done: boolean, assignee: bigint,}
export schema Stats = { type: "object", properties: { open: { type: "integer" }, closedToday: { type: "integer" } }, required: ["open", "closedToday"],};The dashboard — src/routes/+page.pui
Section titled “The dashboard — src/routes/+page.pui”<script lang="ts">import { User, Task, Stats } from "$lib/models.js";import { db } from "$lib/db.server.js";
prop meId: bigint;signal showDone = false;
// Keyed channel — the authority pushes, the cell follows.sync me :: User from `user:${meId}`;
// Live typed query — re-subscribes when `showDone` flips; rows reconcile// individually, membership (insert/remove/reorder) travels separately.sync tasks :: Task[] from query({ where: t => t.assignee == meId && (showDone || !t.done), orderBy: t => t.id, limit: 50,});
// Tier-2 optimistic write: applies locally under an op-id, confirms or// rolls back against the server echo. Never an ambient side effect.mutate rename of me { optimistic(name) { me.name = name; }}
// Opaque server code: no knowable read-set, so the refresh policy is// mandatory — and the expression never ships to the client bundle.sync stats :: Stats from server db.aggregateStats(meId) every 30000;</script>
<h1> {me?.name} <button onclick={() => rename(prompt("name?") ?? me.name)}>rename</button></h1>
<label><input type="checkbox" bind:checked={showDone} /> show done</label><ul> {#each tasks as t (t.id)} <li class:done={t.done}>{t.title}</li> {/each}</ul>
<footer>{stats?.open} open · {stats?.closedToday} closed today</footer>Four declarations, four different liveness contracts — and the :: gate on
every one of them means nothing off the wire touches the DOM unparsed.
Emit the server side
Section titled “Emit the server side”para-kit emit src # artifacts + manifest (+ endpoint, first run)This escape-analyzes the from server expression into
+page.pui.server-sources.pts (the db import hoists there; meId
becomes a wire param that re-keys the channel), writes the flat manifest,
and — once — the conventional endpoint route:
// src/routes/para-sync/+server.ts — written once, yours to editimport { InProcessTransport } from "@lyku/para-sync";import { createServerSourceHost, createSyncEndpoint } from "@lyku/para-kit";import { serverSources } from "$lib/para-sync-manifest.js";
const transport = new InProcessTransport();const host = createServerSourceHost(serverSources, { transport });const endpoint = createSyncEndpoint({ transport, host });
export const GET = endpoint.GET;export const POST = endpoint.POST;Add para-kit emit src --check to CI so a stale artifact fails the build.
Wire the client transport — src/hooks.client.js
Section titled “Wire the client transport — src/hooks.client.js”import { configureSynced } from "@lyku/para-sync";import { createSseTransport } from "@lyku/para-kit";
configureSynced({ transport: createSseTransport({ url: "/para-sync", eventSource: (u) => new EventSource(u), }),});One SSE connection carries every channel the page binds (keys multiplex;
key-set changes coalesce into a reconnect). The stats channel seeds from
the host’s current envelope the moment the stream opens; the query and
keyed channels hydrate from the authority’s baseline.
What this demonstrates
Section titled “What this demonstrates”- Liveness tiers — the
query()stays live because the server knows its read-set; theserveraggregate declaresevery 30000because nobody can know an opaque expression’s; both are visible in the source. - Re-keying — flipping
showDone(or a prop change tomeId) re-subscribes the affected channels, keeping the stale rows on screen until the new baseline lands — no flash of empty. - The write path —
rename(…)is the only way the client changes anything, and it is optimistic-with-rollback, not fire-and-forget. - The boundary —
dbexists only in the generated server artifact; the client bundle provably never contains it.