Skip to content
Fusion

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 / account tables (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 / protocols tables, 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.

Loading diagram...

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:3000

docker 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 user row and an account row holding the hashed password — that account row is what lets someone log in.
  • isSiteAdmin is 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 password password123 — the ~665 register people (real members of the projects), and ~1000 background users. Only the ten Tikab logins get an account row; 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.

Loading diagram...

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.manage and 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-rules permission 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 /sandbox AI 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.