Packages
The Fusion stack is a polyrepo of independently published
@tikab-interactive/fusion-* packages. Each lives in its own repo, is versioned
on its own, and can be installed without pulling in the rest. A product app
composes whichever it needs.
This page covers the tier-1 leaf packages — the ones with no internal
dependency on another fusion-* package, so they can be adopted in any order.
Eight more leaf packages recently joined the set alongside fusion-gis, and
every one of them is exercised by a sandbox in the example app
(/sandbox/<name>) so you can see it run before wiring it into a real feature.
For where these sit in the dependency graph, see Architecture; for the conventions every package follows, Conventions.
Run-anywhere vs. needs-a-service
The split that matters in practice is whether a package needs an external service. A leaf with no infra runs the moment you install it; the rest degrade gracefully until you point them at a backend (the sandbox shows the "off" state too).
The leaf packages
| Package | What it is | Runs | Needs |
|---|---|---|---|
fusion-metrics | Prometheus metrics (prom-client) | server | nothing (METRICS_ENABLED) |
fusion-cache | Valkey cache + Better Auth secondaryStorage | server | Valkey/Redis (VALKEY_URL) |
fusion-jobs | Hatchet background-job enqueue seam | server | Hatchet (HATCHET_CLIENT_TOKEN) |
fusion-realtime | Centrifugo pub/sub — mint token + subscribe | server + /client | Centrifugo (CENTRIFUGO_*) |
fusion-collab | Yjs CRDT + ProseKit collaborative editing | browser | y-websocket (optional) |
fusion-edge-sso | Trusted reverse-proxy SSO header resolution | isomorphic | nothing |
fusion-ldap | LDAP/AD directory logic + group→project sync | logic / ./ldapts | LDAP/AD (live adapter only) |
fusion-import-export | CSV import plan + export + preview table | iso + browser | nothing |
fusion-gis | SWEREF 99 transforms + MapLibre map | logic / browser | nothing |
Each package ships raw TypeScript and follows the graceful-degradation contract: a feature no-ops (or returns a "not configured" result) until its env var is set, so the app always builds and runs.
- fusion-metrics — process + HTTP metrics for a
/api/metricsendpoint, viaprom-client.metricsText()returns the exposition document; server-only soprom-clientstays out of the browser bundle. Off unlessMETRICS_ENABLED=1. - fusion-cache — a small JSON cache (
cacheGet/cacheSet/cacheTtl) over Valkey (a Redis fork; it wrapsiovalkey), plusgetSecondaryStorage()for Better Auth's cross-instance rate limits. Enabled byVALKEY_URL; otherwise reads miss and the app runs uncached. - fusion-jobs — splits the enqueue seam (
isJobsEnabled(), the lightweight.entry) from the heavy Hatchet gRPC client (getHatchet()behind./hatchet, dynamically imported) so the web bundle never pulls in the SDK. Needs a Hatchet engine +HATCHET_CLIENT_TOKEN. - fusion-realtime — the server mints a short-lived Centrifugo JWT
(
issueConnectionToken) and publishes (publish); the browser subscribes via the separate./cliententry (subscribe, wrapscentrifuge). Needs a Centrifugo server +CENTRIFUGO_TOKEN_HMAC_SECRETandCENTRIFUGO_API_KEY. - fusion-collab —
createCollabSession()gives a Yjs document + awareness;defineCollab()binds it to a ProseKit editor (sync, collab-undo, remote cursors). Ay-websocketURL is optional — with none it's a local-only doc, so the sandbox works offline and "turns on" sync when you pass a URL. - fusion-edge-sso — pure, zero-dependency logic that decides whether a request
truly came through the trusted edge proxy (
isTrustedRequest) and resolves the forwarded SSO identity (resolveEdgeIdentity) from Kerberos/SPNEGO or smartcard headers. The proxy does the real auth; this is the app-side trust check. Beyond the sandbox, the example wires it into a real trusted-header sign-in — see Enterprise SSO. - fusion-ldap — reads what an LDAP/AD directory knows about a user and computes
AD-group→project membership diffs (
planProjectMemberships). The.entry is pure logic with anInMemoryDirectoryfake; the live network adapter (createLdaptsDirectory, wrapsldapts) is the separate./ldaptsentry. The example runs this diff on edge sign-in to reconcile project memberships (Enterprise SSO). - fusion-import-export — RFC-4180
parseCsv/toCsv, a typed two-phase import (planImport→ classify each row new/update/skip/error → preview → execute), and theImportPreviewdiff-table React component. The parse/plan halves are isomorphic; download + preview are browser. - fusion-gis — explicitly-named SWEREF 99 ⇄ WGS84 transforms (
swerefToWgs84, wrapsproj4) plus a MapLibreMapViewReact component (tile source is a prop, for air-gapped/self-hosted tiles). The transforms are server-safe; the map is browser-only.
Database-backed packages
Four more @tikab-interactive packages are not leaves — they import
fusion-db's schema and take the app's injected createDb client (no DB env of
their own), so they sit one tier up. They need only Postgres. The first three have
a sandbox; fusion-search powers the live universal search:
| Package | What it is | Try it |
|---|---|---|
fusion-audit | "who did what, when" trail — recordAudit / listAudit | /sandbox/audit |
fusion-settings | runtime key/value app settings an admin can change without redeploy | /sandbox/settings |
fusion-mailer | outbox-backed mailer — records every email, delivers via SMTP (SMTP_HOST) if set | /sandbox/mailer |
fusion-search | universal search — one owner-scoped index, FTS + trigram + vector retrieval fused with RRF | /search |
They're server-only (each stubs its browser entry) and best-effort where it makes
sense (recordAudit and sendEmail never throw). fusion-search degrades to
FTS + trigram with no embedder configured, so it works air-gapped too — see
Universal search. See Architecture for how they fit the
dependency tiers.
The apps package
The four project apps — ProcessN, ProtokollN, NyhetN, WikiN — don't live in the
example app. Their logic ships as one published package,
@tikab-interactive/pulsn-apps, with
one subpath export per app (imported as @tikab-interactive/pulsn-apps/processn, and so
on), plus …/errors.
| Subpath | App | What's in it |
|---|---|---|
…/processn | ProcessN | schema, permissions (typescript-rules), row-filters, server logic, cards |
…/protokolln | ProtokollN | schema, hierarchy-aware permissions, recursive-CTE filters, logic, cards |
…/nyhetn | NyhetN | schema, feed read + Swedish-FTS search |
…/wikin | WikiN | schema, handbook read + collaborative edit + Swedish-FTS search |
There are no relations between the internal apps — each app's project is independent, scoped
only by a projectKey. PulsN holds the canonical project (the "building": key, name, coordinates)
and points to each app's project; that hub (pulsn_project) is a product concern owned by the
consumer, not by any app. So a PulsN project maps to a ProcessN project, a ProtokollN project,
and so on — but ProtokollN is never derived from ProcessN.
The package is logic only — no transport, no UI, no db singleton. Every function takes
an explicit { db, reindex } context plus the caller's id; the consumer wraps it in a
createServerFn (mapping the package's PermissionError → 403) and renders the result with
fusion-ui (which owns every component). That keeps the package framework-agnostic
and fully testable on its own: its suite spins up an ephemeral Postgres and exercises the
real SQL — Swedish FTS, recursive-CTE visibility, the permission tree — with zero dependency on
the app. Flatbox is a separate app; ChattN/Carola stays in the example for now.