Next.js 15 + React 19
App Router, RSC vs client, dev-server invariants, and where state actually lives.
The dashboard is a single Next.js 15 app running React 19. Every page is in the App
Router. There is one Next process per environment; there is no separate API tier.
Route handlers under src/app/api/* are the dashboard's only server surface.
Version pins
| Package | Pin | Why |
|---|---|---|
next | ^15.2.0 | App Router + RSC stable; Turbopack opt-in but not required |
react, react-dom | ^19.0.0 | RSC, useActionState, useOptimistic — used in mutation flows |
typescript | ^5.7.0 | Required for the React 19 type updates |
@types/react | ^19.0.0 | Forced bump from 18 — caused the Vidstack-stable pivot (see Low-latency player) |
React 19 types break some libraries pinned to 18. If you add a third-party React
component package, check its peer-deps before installing. The known fallout so far is
@vidstack/react stable, which is why the public viewer uses a custom chrome wrapper
instead.
App Router layout
Top-level route segments under src/app/:
| Segment | Plane | Purpose |
|---|---|---|
/ | n/a | Root dashboard — fleet overview |
instances/ | Fleet | Node-RED instance management |
flows/ | Fleet | Per-instance flow visualizer + grid + table |
restreaming/ | Restreaming | HoloVids grid, Holoprojector, layouts, panels, groups |
galaxy-map/ | Galaxy Map | Geographic site/link/status renderer |
the-republic/ | Republic | Nations + holdings |
temple-archives/ | Temple Archives | Configuration, farsight-feeds, feed-sync, vessel-categorization |
the-force/ | The Force | Sense (k8s) + embedded external apps |
droidspeak/ | Docs | This handbook (fumadocs) |
registry/ | Settings | YAML registry editor |
nodered/, data/, deploy/, settings/, login/ | various | Smaller operator surfaces |
api/ | server | All route handlers (see Route handlers below) |
Layout files at each level set the chrome (Sidebar, ToastContainer, SoundsProvider).
The root layout.tsx mounts the global providers; nested layouts only add per-plane
chrome (e.g. restreaming/layout.tsx mounts the holovids/holoprojector sub-nav).
RSC vs client components
The split is opinionated:
| Lives on the server (RSC) | Lives in the browser ("use client") |
|---|---|
| Every list page that maps over an array (instances, feeds, layouts) | Anything that uses a hook (useState, useEffect, zustand) |
| Initial data fetch (calls a route handler or reads YAML directly) | Forms, drag-and-drop, video, WebGL, Monaco |
Read of holocron-config.yaml, archives-config.yaml, republic-config.yaml | Live status overlays (zustand subscribers) |
| Auth-gated wrappers that decide whether to render | Sidebar (uses useViewMode + sound provider) |
The client/server boundary is drawn at the smallest component that needs the
browser. app/restreaming/holovids/page.tsx is a server component that fetches the
feed list and passes it into a client <HoloVidsGrid> child — the page itself stays
serializable.
Route handlers
Every server endpoint is a route handler under src/app/api/*. There is no Pages-API
fallback and no Edge runtime — everything is Node runtime by default.
Key route families:
| Route family | Backed by | Pages that consume it |
|---|---|---|
/api/instances/* | instances/registry.yaml + live probe | Fleet, Galaxy Map |
/api/restreaming/* | Restreamer (datarhei/core) + TrailBase + ReductStore | HoloVids, Holoprojector, Galaxy Map |
/api/restreaming/snapshots/* | ReductStore (snapshot history bucket) | HoloVids tiles, Snapshot history tab |
/api/republic/* | TrailBase + republic-config.yaml | Republic |
/api/registry/* | YAML files in repo | Registry editor |
/api/nodered/* | Per-instance Node-RED admin API | Flows |
Three rules every route handler obeys:
- Read-before-write on every mutation. Pull current state, diff in-memory, PUT the merged result. Without this, two operators editing overlapping fields corrupt the upstream config mid-flight.
- Per-item results on bulk routes. Bulk create/delete/command always returns
[{ id, ok, error? }]— never a single boolean. - Audit on every mutating call. One JSONL line to
data/<plane>-audit.jsonlper POST/PUT/DELETE.
State on the client
Three layers, in order of preference:
| Layer | When to use | Examples |
|---|---|---|
| URL | Anything an operator might link to or refresh into | Current panel, current group, active filter |
| zustand | Live status feeds shared across pages | Fleet health, restreaming health, sound mute |
| localStorage | Display preferences only (never source of truth) | Grid/list toggle, HUD slot selection, sidebar collapse |
See zustand for the full store inventory.
The dev-server invariant
R2-D2 has one production-grade local dev server, and it lives on port 3210:
npm run dev:persist # starts/keeps next dev on :3210
npm run dev:persist:clean # nukes .next firstNever run next dev directly from an IDE background shell, and never run two dev
servers on 3210 — both paths corrupt .next and the symptoms (route handlers 500ing
randomly, RSC payloads dropping) take longer to debug than they're worth.
If you see local 500 / Internal Server Error on port 3210 while developing,
dev:persist:clean is the first thing to try.
Build vs dev output isolation
The build path (npm run build) writes into the same .next/ as next dev. Without
isolation, a build kicked off while dev is running occasionally corrupts the
running server's chunk graph. The workaround until output isolation lands: stop
dev:persist before running build.
Extension points
- Adding a route handler — drop a
route.tsundersrc/app/api/<segment>/with aGET/POST/PUT/DELETEnamed export. The middleware path-prefix matching picks it up automatically. - Adding a plane — create
src/app/<plane>/page.tsx(server) plus an entry insrc/lib/nav-config.yaml. Add a stack page here and a components page in Components. - Adding a client widget — put it in
src/components/<plane>/orsrc/components/(shared). Mark only the leaf component with"use client"so the page tree stays serializable.
See also
- Tailwind v4 — design tokens used by every page
- Zustand — where shared state lives
- TanStack Table — the only way to render lists
- Architecture — system-level diagram