Architecture (for contributors)
Inhalt
How Mesh is built internally. Useful if you want to submit PRs, build your own widgets, or are just curious.
High-level overview
┌──────────────────────────────────────────────────────────┐
│ Browser (Next.js Client) │
│ React 19 · Tailwind v4 · Radix UI · React Query v5 │
└──────────────────┬───────────────────────────────────────┘
│ tRPC v11 (HTTP) + JSON
┌──────────────────▼───────────────────────────────────────┐
│ Next.js Server (Node 22) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ tRPC Router Auth Strategy Healthcheck │ │
│ │ /api/widgets/* open/token/ Scheduler │ │
│ │ userPwd/oauth2 p-limit(8) │ │
│ └────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Config Store: zod + proper-lockfile + Migrations │ │
│ └────────────────────────────────────────────────────┘ │
└─────┬─────────┬──────────┬──────────┬─────────┬─────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
/data/ Glances HomeAssistant UniFi Portainer …
config.json :61208 REST + Token API API-Key
Folder structure
apps/web/
├── src/
│ ├── app/ ← Next.js App Router (routes + pages)
│ │ ├── page.tsx ← Dashboard /
│ │ ├── services/ ← /services
│ │ ├── boards/[slug]/ ← /boards/<slug>
│ │ ├── admin/ ← Admin section
│ │ └── api/widgets/ ← Server-side widget endpoints
│ ├── components/
│ │ ├── layout/ ← DashboardHeader, EditModeBar, …
│ │ ├── services/ ← ServiceGrid, ServicesBrowser, PinnedSection
│ │ ├── widgets/ ← 19 widget components + registry.tsx
│ │ ├── infra/ ← InfraNodeGrid, InfraNodeCard
│ │ ├── grid/ ← BoardGrid (react-grid-layout)
│ │ └── ui/ ← Modal, Toast, SortableList, …
│ ├── server/
│ │ ├── config/ ← schema.ts, migrations.ts, store.ts, defaults.ts
│ │ ├── auth/ ← Strategy pattern for 4 modes
│ │ ├── healthcheck/ ← Scheduler + Pinger
│ │ └── trpc/ ← Router + Procedures
│ └── hooks/ ← useLocalSearch, useEditMode, …
├── public/ ← Static assets (logos, brand PNG)
└── package.json
Data flow example: a Glances widget
- User opens dashboard → Next.js renders
app/page.tsxserver-side - Hydration → client mounts React, React Query starts polls
- Widget mounted →
useQueryontrpc.widgets.glances.list - tRPC procedure on the server → reads
config.integrations.glances[] - Per Glances instance →
fetch('http://glances-host:61208/api/4/all') - Response aggregated → JSON back to client
- React Query caches, new poll after
refreshSec
Schema layer
zod is the single source of truth:
ConfigSchema(schema.ts) defines the entire shape- TypeScript types via
z.infer<typeof ConfigSchema> - On every
writeConfig: schema validation - On every
readConfig: schema parse + migration chain
config.json is synchronised with proper-lockfile (atomic writes, OS file lock). Important in the standalone build: proper-lockfile must be listed in serverExternalPackages in next.config.mjs.
React Query as live layer
No WebSocket, no browser-side SSE. Everything is polling via tRPC with:
refetchInterval(default 10–60 s depending on widget)refetchOnWindowFocus: truestaleTimejust belowrefetchInterval
This means no sticky state between tabs, no WebSocket issues behind reverse proxies, no auth state in the socket.
Exception: ESPHome v3 uses server-side SSE (node:http) — but as a one-shot probe, not as a browser stream.
Widget registry
apps/web/src/components/widgets/registry.tsx is the single point for:
kindstring →ComponentdefaultSize/minSize/maxSize(24×20 grid)wideThreshold— column count from which the wide layout is shownsupportsCompact— can the widget show a mini-tile variant?
Adding your own widget:
- Write
apps/web/src/components/widgets/MyWidget.tsx(props:WidgetRenderProps) - Add an entry in
registry.tsxwithkind,displayName, etc. - Add the new kind to the
WidgetKindenum (schema.ts) - No schema bump needed — new widget instances can simply use the new
kind
Auth layer
apps/web/src/server/auth/index.ts exports getAuthStrategy(mode). Each strategy implements:
interface AuthStrategy {
isPublicRoute(path: string): boolean;
authenticate(req: Request): Promise<{ user?: AuthUser, role: Role }>;
}
Routes are checked via adminProcedure (tRPC middleware). The OAuth2 strategy uses openid-client@6 + jose@6 for JWKS validation.
Tests
42+ Vitest tests. Organised per layer:
tests/auth/— OIDC flow, group mapping, JWT validationtests/healthcheck/— pinger logictests/migrations/— migration chain round-trip
Build
Multi-stage Dockerfile:
deps— pnpm installbuild—pnpm build(Next.js standalone output)runtime— copy.next/standalone+.next/static+public+ entrypoint
Standalone output bundles all dependencies — the final container has NO node_modules/.
Roadmap ideas
- Optional WebSocket for sub-1-second live updates (today: 10 s polling)
- Plugin architecture with dynamic widget loading from separate repos
- Federated boards across multiple Mesh instances via Tailscale/WireGuard