Skip to content

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.

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.json
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"],
};
<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.

Terminal window
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 edit
import { 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.

  • Liveness tiers — the query() stays live because the server knows its read-set; the server aggregate declares every 30000 because nobody can know an opaque expression’s; both are visible in the source.
  • Re-keying — flipping showDone (or a prop change to meId) re-subscribes the affected channels, keeping the stale rows on screen until the new baseline lands — no flash of empty.
  • The write pathrename(…) is the only way the client changes anything, and it is optimistic-with-rollback, not fire-and-forget.
  • The boundarydb exists only in the generated server artifact; the client bundle provably never contains it.