The example app
fusion-meta/example is the reference TanStack Start
app — the canonical way the stack is assembled. It's branded PulsN (the app name;
Fusion is the stack underneath it), and it models a real domain: Tikab's portfolio
of active construction projects — nine real assignments (Slussen, Slakthusområdet,
Hisingsbron, …) and the engineering disciplines around them. What began as "Hello World
- Auth" has grown into a near-complete application with two halves:
- A product app (PulsN) — a chat-first portfolio landing, a dashboard, sign-up/login (plus optional enterprise SSO for air-gapped deployments), a protected app shell, in-app notifications, file uploads, a user directory, a full admin area, four in-app domains (ProcessN, ProtokollN, NyhetN & WikiN), and an integrated external service (Flatbox).
- A sandbox gallery — one page per leaf package and per AI feature, each running in isolation and degrading gracefully when its backing service isn't configured.
If you want to know how a given package is meant to be wired into a real app, this is where to look. To build something of your own on top of it, jump to Building a feature.
Who owns what
It helps to draw the line between the stack and the app, because it answers a question that trips people up: is the example in charge of users, projects, permissions?
- The Fusion packages own the foundation — the generic, every-app plumbing: the
database client and the
user/session/accounttables (fusion-db), auth (fusion-auth), UI (fusion-ui), storage, mailer, AI, and so on. You install these; you don't write them. - The app owns its domain — everything specific to this product: the
projects/tasks/protocolstables, their migrations, the permission rules, and the server functions and pages that use them. The example is entirely in charge of its own projects — there is no "fusion-projects" package.
That last point is deliberate. A "project" means something different in every product (a task board here, a meeting workspace there), so the stack doesn't try to own it — each app defines its own project tables and its own membership/permission model. That's exactly why the example carries two different ones, ProcessN & ProtokollN, to prove the pattern stretches. Build a new app on Fusion and the foundation is given; the domain — your projects, your rules — is yours to model.
Run it
cd example
bun install
cp .env.local.example .env.local # set BETTER_AUTH_SECRET (AI vars optional)
docker compose up -d # Postgres :5433 · MinIO :9000 · Valkey :6379 · Hatchet :8888 · Kreuzberg :8000
bun run db:generate && bun run db:migrate
bun run db:seed # Tikab logins + register people + the nine-project portfolio (ProcessN/ProtokollN, faker)
bun run dev # http://localhost:3000docker compose brings up the services the app uses — Postgres (with PostGIS), MinIO for
file storage, Valkey for the cache, the self-hosted Hatchet scheduler (the
proactive agent's timer and the attachment-processing worker), and
Kreuzberg for extracting text from attached PDFs/Office docs. The remaining sandbox
services (Centrifugo, a y-websocket server, an LLM) are optional; each sandbox shows you what
to set when it's off.
Sign in
Signing in lands you on /home — the chat-first portfolio,
backed by the proactive agent. Sign up, or use a seeded account:
every login's password is password123, and the site admin is
andre.holmstrom.tikab@p4o.se (André Holmström, uppdragsansvarig). The other
named logins are his nine Tikab colleagues — same fornamn.efternamn.tikab@p4o.se
pattern — Sara Lindqvist (Konstruktör MEK), Johan Bergström (Beräkningsingenjör), Emma
Nilsson (BIM-samordnare), Linnéa Sjöberg (Projektledare), Mattias Ek (VDC-specialist),
Klara Öberg (Uppdragskoordinator), and three more. See the domains
seed for who can do what.
Users & accounts
There's one pool of users for the whole app — the same accounts sign into the
product pages, the admin area, and every domain. A user is a row in the user
table from fusion-db's foundation schema (id,
name, email, plus an isSiteAdmin flag and a deactivatedAt); the app adds no
user table of its own.
How a user comes to exist:
- Sign-up / sign-in go through fusion-auth
(Better Auth). Signing up creates the
userrow and anaccountrow holding the hashed password — thataccountrow is what lets someone log in. isSiteAdminis a single boolean on the user row — the one global role. Everything else is per-project membership (the domains). The Admin page grants or revokes it.- The seed (
bun run db:seed) creates the people you test as — the ten Tikab logins (a site admin plus nine colleagues), all with passwordpassword123— the ~665 register people (real members of the projects), and ~1000 background users. Only the ten Tikab logins get anaccountrow; everyone else has no password and can't log in — they exist to give the projects and the Admin table realistic volume.
In short: the foundation defines the table, fusion-auth creates real logins,
isSiteAdmin is the only site-wide role, and the seed fills in demo people. What a
user may do past signing in is decided by authorization and
their project memberships. To read the current user in a page, see
Building a feature.
The auth seam
The app builds its database client with createDb(schema, url), then owns the
Better Auth instance via the decoupled factory — createAuth({ db }) plus
createAuthSession(auth, db). Sign-up persists through fusion-db; the
_authed layout guards everything under it and puts the user on the route context.
The product app
The signed-in area lives in the fusion-ui app shell (AppShellLayout) — a sidebar
of pages plus a header with a notification bell, the language switcher, theme toggle
and user menu. The shell is mobile-friendly: the sidebar collapses behind a burger and
the layout is tuned for iOS (see Capabilities).
Home — the portfolio
/home — where you land after signing in. Not a card dashboard and not a buried chat box,
but a chat-first portfolio: a calm, minimal surface where Carola — the assistant — is
the focus. A large serif greeting, a scope switch (Alla projekt / Allmänt), Carola's
one-line briefing with the single most-urgent thing, and a prominent composer — that's it. The
full picture comes from Carola on demand, in the conversation (a chip or a question),
rather than pre-dumped as a dashboard. The portfolio's job is "what is urgent across your
buildings" → pick one, which scopes the whole app to that building (/project/$key). It
renders deterministically from owner-scoped server reads, so the landing works offline /
air-gapped.
Carola is inline — one canvas that expands and reflows as the conversation opens. The
inline rule is about in-page widgets: never a modal, popover or floating in-page widget. The
one deliberate exception is floating Carola — the Document-PiP pop-out, which lifts the
live conversation into a real, separate OS window (Carola). She answers from
the same data the autonomous agent uses (findings, deadlines, memory) plus the
building's documentation and news (NyhetN & WikiN) and any files you attach to the
conversation — those attachments are per-conversation (Carola,
document RAG) — through owner-scoped tools, never
invention. Conversations persist in the sidebar history and are addressable by URL
(/c/$id for general/portfolio chats, /project/$key/c/$id inside a building). In the
background a scheduler still wakes the model on an interval, runs checks (your deadlines,
inbox, system health) and pushes in-app notifications (the header bell) when something
matters. The harness lives in fusion-ai; the operator guide (kill switch, digests,
GDPR, metrics) is Proactive agent.
Dashboard
/dashboard — the front page (and first nav item). It lays out the signed-in user's
work as summary cards aggregated across every integration (ProcessN, ProtokollN,
Flatbox) through one shared card interface. The server function streams the cards
(it's an async generator): the fast in-process cards render immediately while the
slow cross-server Flatbox card arrives after, and a provider that fails is skipped so
one integration can't break the page. To build a page like this yourself, see
Building a feature.
Hello
/hello — greets the signed-in user; visible to anyone with a session. The original
"Hello World" slice, kept as the simplest possible protected page (the chat-first
Home is now the landing).
Files
/files — pick an image and upload it. The uploadImage server function checks
the MIME type (PNG/JPEG/WEBP/GIF), stores the object through fusion-storage
(MinIO locally, Azure Blob in the cloud), and returns a URL served back through the
/api/files proxy route. Server-only, so the storage driver never reaches the
browser bundle. This is the bare fusion-storage upload demo — distinct from the files you
attach to a Carola conversation (Carola), which are per-conversation and
RAG-indexed.
Directory
/directory — a list of the users you're allowed to see. It's open to every
signed-in user, but the rows are scoped server-side by a
typescript-rules Drizzle filter: a site admin lists everyone,
everyone else lists only themselves. The row scoping is the access control — not
a role gate on the page.
Admin
/admin — the site-admin-only back office (the route guard redirects everyone else to
/home). It's built on a generic AdminResource framework (a fusion-ui
AdminShell + the example's resource layer), so each entity gets a consistent table
with CRUD and CSV import/export:
- Users — bulk activate / deactivate / rename / grant-admin, gated by
users.manageand re-checked on the server; it refuses to deactivate or de-admin yourself. The ~1000 seeded background users give the table realistic volume. - Settings (
/admin/settings) — runtime app config (app name, default language, sign-ups, support email, banner) via fusion-settings. - Audit (
/admin/audit) and Outbox (/admin/outbox) — the fusion-audit change feed and the fusion-mailer outbox. - ProcessN / ProtokollN resources (
/admin/processn/*,/admin/protokolln/*) — manage the domain entities directly (projects, tasks, statuses, companies, protocols, points, …).
Domains: ProcessN, ProtokollN, NyhetN & WikiN
The in-app domain modules are the most substantial part of the app.
ProcessN (/project/$key/processn/*, task management) and
ProtokollN (/project/$key/protokolln/*, meeting protocols) are
the best demonstration of real authorization on the stack. NyhetN
(/project/$key/nyhetn, the building's news feed) and WikiN
(/project/$key/wikin, a rich-text handbook) are per-building knowledge surfaces the
proactive agent answers questions from. Each has its own page — start at
The apps.
Flatbox (an integrated service)
/flatbox/* — projects, review sessions, packages and comments from Flatbox, a
separate app the example consumes over HTTP. Unlike the in-app domains, Flatbox
owns its own data and permissions; rows arrive already scoped and canEdit-tagged,
and the app renders them read-only. It's the worked example of integrating an
external service → Integrating a service.
The sandbox
Separately from the product app, /sandbox is a gallery that exercises each leaf
package and AI feature on its own. It's how you confirm a capability works (and a
service is configured) before building a feature on it →
Sandbox.
Capabilities on show
The example also wires the stack's cross-cutting concerns:
- Authorization — the Directory and Admin pages, plus the
domain row-scoping, all run on one
typescript-rulespermission model. - Internationalization — Swedish + English via Paraglide; the switcher
in the header. The UI defaults to Swedish (
baseLocale); the seeded data and the proactive agent's output are Swedish too, and English is one click away. - AI & the proactive agent — the
/sandboxAI pages run prompts through fusion-ai (provider toggle: local Ollama vs hosted OpenRouter); the agent layers autonomous scheduled checks, pgvector memory and in-app notifications on top, and answers natural-language project questions from the NyhetN / WikiN knowledge base via Postgres full-text search. - Carola — the conversation-first shell: a per-conversation header (rename / star / add-to-project / delete + Files + pop-out), per-conversation file attachments, the companion rail, and floating Carola (Document PiP — the live conversation in a separate OS window, the one exception to the inline-only rule).
- Universal search — hybrid FTS + trigram + vector over one owner-scoped search index (RRF in fusion-search), with three distinct verbs: Ask Carola, Find a record, Scope the app to a building.
- Mobile / iOS — the shell is responsive (burger nav, avatar-only header on
phones) and hardened for iOS: 16px inputs so Safari doesn't zoom on focus,
viewport-fit=cover+ safe-area insets.
How the packages are wired
The fusion packages are consumed as source — linked straight from their sibling
repos via tsconfig.json paths, with Vite resolving them through
resolve: { tsconfigPaths: true } rather than installed from the registry.
drizzle-orm, react, Mantine and @tanstack/ai are deduped to the app's single
copy so the linked packages share one instance. fusion-ui's UI stack (Mantine, Tabler,
Tiptap) is declared by fusion-ui as peer dependencies and installed here — it can't
be dropped from the app while fusion-ui is source-linked, or SSR loads two Reacts
(Conventions). The exception is
@tikab-interactive/typescript-rules, which is installed from the registry and
bundled for SSR via ssr.noExternal. See Getting started for
the full linking story, and Conventions for why.
Deploy
Infrastructure-as-code is Bicep (Azure Container Apps + PostgreSQL Flexible Server +
Blob storage) with a cross-platform deploy.ts — see example/infra/ and
Deploy. It runs the same on Windows and macOS/Linux.