@identikey/component-kit
v0.2.0
Published
Contract-first (oRPC) full-stack slices + Svelte 5 components for any Web-standard host. Framework-neutral by default; Elysia + Identikey auth are optional.
Readme
@identikey/component-kit
Contract-first (oRPC) full-stack slices + Svelte 5 components for any
Web-standard host. Bring your own backend and your own auth — the kit
mounts into your server as a plain (Request) => Response handler and reads
your users through a neutral auth port. Elysia and IdentiKey auth are
optional, batteries-included fast paths, not requirements.
Status: v0.1.x. The contract-first surface (oRPC router, fetch adapter, typed client, SSE event iterators) is the default path and is proven in a production host (
negotiated, a SvelteKit app with its own session auth). The single reference component isGpuControlCard— dumb, prop-only, and skinnable. More components will follow the same pattern.
Why this exists
The kit is top-of-funnel for IdentiKey self-sovereign, key-based auth and the IdentiKey key-managed proxy: adopt the components at zero commitment with your existing stack, meet IdentiKey as the one-switch auth upgrade, graduate to managed keys when you want them. Open on the interface axis, opinionated on the default axis — on purpose.
Read docs/VISION.md for the funnel model and ADR 0001 for the auth posture (neutral port, IdentiKey flagship, capability negotiation).
What it is
Something between a framework and middleware. Each "component" is a contract-first slice:
- A valibot + oRPC contract — the single source of truth both ends derive from
- Server handlers (
implement(contract)) over a provider port you implement — compile-time checked against the contract, no HTTP framework baked in - A rune-backed controller that drives the contract-typed client + a resumable event stream into one reactive object
- A Svelte 5 component (dumb, props-only, skinnable) that renders it
The host mounts one catch-all route and implements two small ports
(AuthPort, GpuProvider). In exchange it gets: end-to-end typed RPC, a
resumable typed event stream, and ready-to-render components — while keeping
its own auth, audit logging, and error semantics.
Host integration (bring-your-own-backend)
Proven wiring, from the negotiated reference integration:
1. Implement the ports and build the handler
// src/lib/server/kit.ts
import {
createGpuRouter,
createKitFetchHandler,
memoryBus,
type AuthPort,
type GpuProvider
} from '@identikey/component-kit/server';
const auth: AuthPort = {
// Read YOUR session (cookie, JWT, whatever) from the raw Request.
resolve: async (request) => /* { user, session } | null */,
isAdmin: (user) => /* your admin rule */,
describe: () => ({ kind: 'my-session', methods: ['token'] })
};
const provider: GpuProvider = {
/* your RunPod / Lambda / in-house impl. Mutations receive the acting
user (GpuActor) so your audit trail survives. Throw ORPCError with
data.code ('gpu_busy', ...) to keep your failure UX. */
};
export const bus = memoryBus(); // publish to it from workers/schedulers too
const router = createGpuRouter({ provider, bus });
export const handleKitRequest = createKitFetchHandler(router, {
auth,
prefix: '/api/kit'
});2. Mount on one catch-all route
// src/routes/api/kit/[...rest]/+server.ts (SvelteKit; any Web-standard host works)
import { handleKitRequest } from '$lib/server/kit';
import type { RequestHandler } from './$types';
const handle: RequestHandler = async ({ request }) =>
(await handleKitRequest(request)) ?? new Response('Not Found', { status: 404 });
export const GET = handle;
export const POST = handle;
export const DELETE = handle;3. Wire the UI with the contract-typed client
<script lang="ts">
import { createKitClient, createGpuController } from '@identikey/component-kit/client';
import { GpuControlCard } from '@identikey/component-kit/components';
const client = createKitClient({ url: '/api/kit' });
const gpu = createGpuController({
client,
isAdmin: () => page.data.user?.isAdmin ?? false,
pollMetricsMs: 5000 // optional: keep uptime/cost ticking between events
});
$effect(() => {
gpu.start();
return () => gpu.stop();
});
</script>
<GpuControlCard
metrics={gpu.metrics}
pods={gpu.pods}
isAdmin={gpu.isAdmin}
mutating={gpu.mutating}
mutationError={gpu.mutationError}
podsLoading={gpu.podsLoading}
podsError={gpu.podsError}
pendingPodIds={gpu.pendingPodIds}
autoShutdown={gpu.autoShutdown}
autoShutdownSaving={gpu.autoShutdownSaving}
autoShutdownError={gpu.autoShutdownError}
onAction={gpu.onAction}
onAdopt={gpu.onAdopt}
onTerminate={gpu.onTerminate}
onRefresh={gpu.onRefresh}
onReconcile={gpu.onReconcile}
onDismissError={gpu.onDismissError}
onSaveAutoShutdown={gpu.onSaveAutoShutdown}
/>Types hold end-to-end: the client is typed from the contract
(ContractRouterClient<GpuContract>), not from the server or its framework,
and implement(contract) compile-checks every handler. The guarantee doesn't
evaporate when a host serves the routes from its own stack — that's the point.
Why dumb components
Components take only props + callbacks. The controller, not the component, owns the data lifecycle. This means:
- Components test in isolation (no fetch mocking)
- Hosts can swap the controller for their own state without forking the UI
- The same component works whether you wire it with runes, stores, or TanStack Query
Skinning
Components render with plain, scoped-CSS markup by default — zero host deps. A host with its own design system overrides the look without forking the component, by passing optional skinning props:
Button/Checkbox— inject your own primitives. They must satisfyKitButtonProps/KitCheckboxProps(exported from/components); a thin wrapper around shadcn-svelte'sButton/Checkboxis usually a few lines.chrome— aSnippet<[header, content]>that wraps the card shell, so you can render the header + body inside your ownCard(e.g. shadcnCard.Root>Card.Header/Card.Content).
<GpuControlCard {...gpu} Button={MyButton} Checkbox={MyCheckbox}>
{#snippet chrome(header, content)}
<Card.Root>
<Card.Header>{@render header()}</Card.Header>
<Card.Content>{@render content()}</Card.Content>
</Card.Root>
{/snippet}
</GpuControlCard>The metrics list, drift rows, error alert, and auto-shutdown form still use the kit's scoped CSS — inject more hooks here as real hosts need them.
Auth: neutral port, IdentiKey flagship
Any host implements the floor:
interface AuthPort {
resolve(request: Request): { user, session } | null | Promise<...>;
isAdmin?(user): boolean;
describe(): { kind: string; methods: ('oauth'|'passkey'|'token'|'signing')[] };
}describe() is capability negotiation: components progressively enhance —
passkey/signing affordances light up only when the active provider advertises
them. The IdentiKey flagship provider (in progress, component-kit-on6)
implements the floor and extends past it: passkey/WebAuthn ceremonies, OAuth,
per-request key-signing (proof-of-key, not a cookie check), and proxy hooks.
See ADR 0001.
If a provider verifies signatures over the request body, resolve() must not
consume the body stream the handler needs — verify over headers + a body
hash, or clone the request.
Event bus
Events ride the same contract as the RPC calls: client.events({ topics })
returns a typed async iterator (oRPC event iterator over SSE). Reconnects
resume via Last-Event-ID from a ring buffer (default 256 envelopes); each
event's meta carries the monotonic id. Topic filtering supports wildcards
(gpu.session.*).
Bus interface is host-swappable:
import type { EventBus } from '@identikey/component-kit/server';
const bus: EventBus = /* your Redis-backed impl */;
createGpuRouter({ provider, bus });The bridge is load-bearing. The kit only publishes session events for
mutations that flow through its own router. Transitions your host initiates
out-of-band — auto-shutdown, health-check failures, background jobs — reach
clients only if you publish them: bridge your internal state broadcasts
onto the bus as gpu.metrics snapshots (see the host integration example).
A host that skips the bridge gets a card frozen on stale state between manual
refreshes; pollMetricsMs on the controller is a mitigation, not a substitute.
See docs/operating-constraints.md for the
memoryBus single-instance constraint, the multi-instance EventBus story, and
the semver / contract-compatibility policy (what's frozen vs. free to change).
Authorization model
Enforced by oRPC middleware reading the per-request context the adapter
builds from your AuthPort:
| Procedure | Requires |
|---|---|
| metrics, listPods, events | signed-in user |
| start, stop, restart | signed-in user |
| terminate, adopt, getAutoShutdown, saveAutoShutdown | admin |
Admin is whatever AuthPort.isAdmin(user) returns. Mutations pass the acting
user through to your provider for audit logging.
The optional Elysia fast path
Greenfield hosts that want zero boilerplate can mount the kit's own Elysia
app (with the IdentiKey provider, CSRF guard, and Eden client) via the
@identikey/component-kit/elysia entry — createKit({ auth: identikey({...}),
gpu: { provider } }) and one catch-all. Elysia, Eden, and
@simplewebauthn/server are optional peers: hosts on the contract-first
path install none of them.
Package layout
src/
contracts/ valibot schemas + the oRPC contract router (SSOT)
server/
router.ts implement(contract) → handlers over GpuProvider + bus
adapters/fetch.ts (Request) => Response — bring-your-own-backend
adapters/elysia.ts optional turnkey mount
auth/ AuthPort context/middleware; Identikey Elysia provider
providers/gpu.ts GpuProvider port (host implements)
bus/memory.ts in-process event bus with ring-buffer replay
client/
createKitClient.ts contract-typed oRPC client
gpu.svelte.ts createGpuController() — rune-backed state for GpuControlCard
events.svelte.ts raw EventSource fallback
auth.ts oauthStart, signInWithPasskey, logout
components/
GpuControlCard.svelte, KitButton, KitCheckbox, skin.tsExported subpaths: /server, /client, /components, /contracts, and the
optional /elysia.
Tests
bun run check # tsc --noEmit
bun run test # vitest runTests cover the contract round-trip (client ⇄ fetch adapter in-process), auth gating, actor threading, event replay/resume, bus behaviour, sessions, and compile-time contract conformance.
Consuming locally
Until the npm publish pipeline lands (component-kit-6u5), hosts consume the
source tree directly:
// host package.json
"@identikey/component-kit": "file:../relative/path/to/component-kit"Two things make the symlinked-directory dep safe (learned the hard way):
- Align
svelteversions between this repo and the host. TypeScript realpath-resolves the symlinked source, so a version skew means two svelte copies and "two different types named Snippet" errors undersvelte-check. - In the host's Vite config, add the kit to
ssr.noExternal(it ships.sveltesource) and dedupe the shared peers:resolve: { dedupe: ['svelte', 'valibot'] }.
A packed tarball (bun pm pack → file:...tgz) also works and avoids the
symlink caveats, but bun caches tarballs by filename — bump the version on
every repack.
Not yet
- npm publish (
component-kit-6u5) — blocks any consumer that deploys from a repo image (relative paths don't resolve in CI/prod builds) - IdentiKey flagship provider on the neutral port (
component-kit-on6) — ceremonies currently exist only in the Elysia/Eden legacy path - Component-side capability negotiation UI (
component-kit-26z) - Redis bus adapter (interface is ready; just needs the impl)
- Per-instance scaling (in-memory user cache + pending-ceremony state)
- Storybook stories
- More components beyond GpuControlCard
