Build your own app
An app on Real Life Stack provides three things: a Connector Die Implementierung des DataInterface für eine konkrete Datenquelle, ergänzt um unterstützte Capabilities. Die Steckstelle des Stacks nach unten. GlossarConnector The implementation of the DataInterface for one data source, plus the capabilities it supports. The stack's socket downward. Glossary to its data, a router for the address and the frame from the toolkit. Header, tabs, panel, create, detail view and the seven modules (feed, list, kanban, calendar, map, graph, resonance) come ready-made. All the code below lives as a living example under examples/first-app/ in the repository and is built and tested in every CI run.
This is what the result looks like, with the same building blocks, in Storybook:
A community garden on the frame: space, modules, one item in the calendar and on the map. The sample data is fictional. Changes last only for this session.
1. Packages
Three packages from Real Life Stack and four from outside. react-router-dom carries the address, maplibre-gl draws the map.
{ "name": "first-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build", "typecheck": "tsc --noEmit", "test": "vitest run" }, "dependencies": { "@real-life/data-interface": "^0.4.0", "@real-life/mock-connector": "^0.2.3", "@real-life/toolkit": "^0.4.0", "maplibre-gl": "^5.24.0", "react": "^19.0.0", "react-dom": "^19.0.0", "react-router-dom": "^7.1.1" }, "devDependencies": { "@tailwindcss/vite": "^4.1.0", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", "jsdom": "^26.1.0", "tailwindcss": "^4.1.0", "typescript": "~5.7.2", "vite": "^6.0.7", "vitest": "^4.1.0" }}The three packages come from npm, in the repository as well; the lockfile records the exact version. The frame and the seven modules are included in @real-life/toolkit.
2. The data and the address: main.tsx
import { StrictMode } from "react"import { createRoot } from "react-dom/client"import { createBrowserRouter, RouterProvider } from "react-router-dom"import { MockConnector } from "@real-life/mock-connector"
import { App } from "./App"import "./index.css"import "maplibre-gl/dist/maplibre-gl.css"
async function start() { // Die Daten: ein Space, ein Mensch, ein Item — im Speicher, für den Anfang. const connector = new MockConnector({ users: [{ id: "mira", displayName: "Mira" }], groups: [{ id: "garten", name: "Gemeinschaftsgarten", data: { modules: ["calendar", "map", "collection"] } }], groupMembers: { garten: ["mira"] }, items: [{ id: "erntefest", type: "event", createdBy: "mira", createdAt: "2026-09-01T10:00:00Z", data: { title: "Erntefest", content: "Wir teilen unsere Ernte.", start: "2026-09-26T14:00:00+02:00", position: { type: "Point", coordinates: [13.4, 52.5] } }, }], groupItems: { garten: ["erntefest"] }, }) await connector.init() connector.setCurrentGroup("garten")
// Die Adresse: /{space}/{modul}/{item} — der Rahmen liest sie, die App stellt nur den Router. const router = createBrowserRouter([{ path: "*", element: <App connector={connector} /> }])
createRoot(document.getElementById("root")!).render( <StrictMode> <RouterProvider router={router} /> </StrictMode>, )}
void start()Line by line:
MockConnectorkeeps the data in memory: one person (users), one Space Der gemeinsame Arbeits-, Mitgliedschafts- und Sichtbarkeitskontext. Im Datenvertrag heißt er Group. GlossarSpace The shared context for work, membership and visibility. In the data contract it is called Group. Glossary (groups) with its modules in tab order (data.modules), the membership (groupMembers) and one item (items) assigned to the space (groupItems). The item has a date (start) and a place (position); that is why it appears in the calendar and on the map, as the same object.await connector.init()waits until the connector is ready.setCurrentGrouppicks the space the app shows first.createBrowserRouterwith a single route*: the frame reads the address/{space}/{module}/{item}itself. The app owns only the router; that is the part that stays with it (spec 01, “what stays with the app”).- The two CSS imports: your stylesheet and the map’s.
3. The frame: App.tsx
import type { DataInterface } from "@real-life/data-interface"import { ConnectorProvider } from "@real-life/toolkit"import { MapLibreAdapterProvider } from "@real-life/toolkit/maplibre"import { RoutedAppFrame } from "@real-life/toolkit/router"
// Die App stellt drei Dinge: den Connector, die Karten-Engine und den Rahmen.// Kopfzeile, Tabs, Panel, Erstellen, Detail und alle Module kommen aus dem Toolkit.export function App({ connector }: { connector: DataInterface }) { return ( <ConnectorProvider connector={connector}> <MapLibreAdapterProvider> <RoutedAppFrame fallbackModule="collection" /> </MapLibreAdapterProvider> </ConnectorProvider> )}ConnectorProvidermakes the connector reachable for every surface below. Surfaces ask hooks, never the connector directly.MapLibreAdapterProvideris the map engine, the one line an app writes for the map. Without it the map module shows the note “no map engine”.RoutedAppFrameis the frame: providers, the shared panel, the header with space management, the tabs from the space’s register, the focus in the address and the guard against losing unsaved input.fallbackModulesays which module shows an item when none of its fields picks another: here the list.
No register, no module, no detail is wired here. The register contains the toolkit’s modules without further ado; a module of your own comes later as an addition (page “Your own module”, to follow).
4. Styles
@import "tailwindcss";@import "@real-life/toolkit/styles/globals.css";@source "../node_modules/@real-life/toolkit/dist";@custom-variant dark (&:is(.dark *));Tailwind, the toolkit’s tokens (globals.css) and the instruction to collect the classes in the built toolkit (@source). Without the third line the frame has neither spacing nor colours.
5. Start
In the root of the repository:
pnpm installpnpm dev:first-appOutside the repository it is pnpm install && pnpm dev in your own directory; in both cases the packages come from npm.
In the browser: the space “Gemeinschaftsgarten” with three tabs. The calendar shows the harvest festival on 26 September, the map shows it in Berlin, the list shows it as a card. Clicking the item opens the detail view in the panel, the address becomes /garten/calendar/erntefest. The plus button bottom right creates a new item, with all types; in the calendar it suggests “Termin” (event). The header lets you create another space.
What the app does not do
- It stores nothing: the
MockConnectorlives in memory. Another connector (local, Supabase, Web of Trust) swaps the data source without changing a line inApp.tsx(page “Switch connectors”, to follow). - It has no module of its own. Adding one takes three steps: a view, a register entry, a hint (page “Your own module”, to follow).
- It signs nobody in. Sign-in, permissions and encryption come with the connector that can do them; the frame then shows what the connector allows.
Where to go next
- The spec on the frame: 01 App composition, “what stays with the app” (German).
- In Storybook: RLS → App shows register, host, loading contract, focus and create one by one.
- Every hook a surface can ask: All hooks and the reference.