Skip to content

🧭 Platform overview

Seven own web apps share one platform: a Rust + React stack whose apps are local-first PWAs. They are built on a Windows dev machine, published to a private registry, and deployed by git push to the ODROID-C4. shared-core runs the fleet. This tab shows how it fits together; the install runbooks are under Services and Apps.


🚀 One deploy, end to end

DEV MACHINE Windows 11 docker build linux/arm64 · native or cross tag <app sha>.<platform sha> 1 git push the commit the image names wait until /api/version names it deploy.ps1 runs 1 → 7 app repo + sibling ../platform USERS browser · phone installed PWA, works offline ODROID-C4 Debian trixie · arm64 · rootless podman NGINX 443 · TLS headers rate limits one vhost per site Registry 127.0.0.1:10090 every tag kept bare repo post-receive hook podmanctl pull · re-own · restart <app> service quadlet units · systemd --user · <app>_svc app Rust + SPA :100xx postgres data/postgres-db sidecars tmdb · gmaps osrm data/ bind mounts under /media/ssd/podman/<app> restarts with backoff · a failed healthcheck kills it one Linux user and podman store per service 2 3 4 5 6 7 HTTPS 127.0.0.1 :100xx
One deploy: the image is published first, the commit pushed second, production checked last.
  1. Build: deploy.ps1 builds the app's linux/arm64 image on the dev machine. The platform crates compile into the binary inside it (Build & deploy).
  2. Publish: it pushes the image to the registry, tagged <app sha>.<platform sha>.
  3. Push: then, and only then, it git pushes the commit.
  4. Hook: the bare repo's post-receive hook runs podmanctl --deploy <app> <sha>.
  5. Pull: podmanctl pulls the clone, then the image. A missing image refuses the deploy before anything stops.
  6. Restart: it restarts the app's units on the new image, and reloads nginx only when nginx/ changed.
  7. Wait: deploy.ps1 polls /api/version until it names the pushed commit.

Every step, check and exit code: the life of a commit.


🧩 The four pieces

🛠️ Platform: the shared code

PLATFORM REPO ..\platform · 0.26.0 EACH OWN APP 7 repos crates/ 8 Rust crates · platform-* src/backend the Rust binary path deps packages/ 7 npm packages · @platform/* src/frontend the React SPA file: deps contracts/ the rules every app follows CLAUDE.md reads the rules, never the source read conformance/ HTTP + browser suites local stack run-local.ps1 + its nginx checks refapp/ golden app · service platform new app /new-app copies refapp seeds
  • What: 8 Rust crates, 7 npm packages, 12 contracts, the conformance suites and RefApp, the golden app (service platform).
  • How: an app takes the crates by path and the packages by file: against the sibling clone; its container build gets the clone as --build-context platform=…. Nothing is published to crates.io or npm.
  • Version: PLATFORM_VERSION (0.26.0) names the release; each app records the one it runs in PLATFORM_ADOPTED, and its /api/health names it.
  • More: Crates & tooling · the Platform service

📦 Registry: the image store

DEV MACHINE docker push registry.fum-server.fr after docker login one tag per deploy <app>:<app sha>.<pf sha> sidecars share it e.g. mediascope-tmdb ODROID-C4 NGINX TLS basic auth /v2/ only registry-server distribution (registry:2) 127.0.0.1:10090 → :5000 data/registry on the SSD registry-gc.timer Sun 04:00 · stop, sweep, start podmanctl as <app>_svc pulls on loopback checks the labels refuses nothing published → nothing stopped pull
  • What: the upstream distribution registry, one container on 127.0.0.1:10090.
  • Why: the ODROID never compiles an own app. A cold build there took about 20 minutes per app.
  • How: dev machines push to registry.fum-server.fr (TLS + basic auth); podmanctl pulls on the loopback address and checks the image's two revision labels.
  • Rule: DEPLOY_REGISTRY in an app's env file opts it in. Once set, a missing image refuses the deploy instead of building; deleting the key on the server is the rollback.
  • More: the Registry service

🧠 shared-core: the control room

SHARED-CORE the fleet's control room knows fleet.json own-projects.md cloned-projects.md CLAUDE.md memory/ checks fleet-check.ps1 nginx-check.ps1 fleet-lockfiles.ps1 fleet-deploy-script.ps1 fleet-version.ps1 runs /fleet-rollout /new-app start-rollout.ps1 records rollouts/<version>.md · .fleet/ plans and logs · nonregression-plan.md EVERY REPO 15 + docs 7 own apps + platform deploy.ps1 · run-local.ps1 dev-nginx.psm1 copies 7 cloned apps their own deploy.ps1 and run-local.ps1 family byte-identical fleet-deploy-script.ps1 compares each copy with its canonical one
  • What: no application code. It holds the inventories, fleet.json, the fleet scripts, the rollout commands, the rollout records and the sessions' memory.
  • How: its scripts read fleet.json and act on the sibling clones; the rollout session runs in it.
  • Rule: app repos stay autonomous. Shared files are byte-identical copies in each repo, checked by fleet-deploy-script.ps1, and nothing at runtime depends on shared-core.
  • More: Crates & tooling · Release & rollout

🧱 Own apps: one shape, seven times

IN THE BROWSER installed PWA app pages React 19 · TypeScript · lucide icons @platform/* sync · offline · events auth-ui · users-ui · api-core service worker · IndexedDB store outbox · file queue on the device rows of every collection eager images · the shell unsent edits and files ON THE SERVER one container app routes axum · tokio · sqlx platform-* crates auth · sync · files · events http · mail · app · http-client sessions · MFA · error envelope cache tiers · security headers postgres the app's tables users · sessions · shares files under data/ REST /api/* POST /api/sync SSE /api/events same origin · nginx in front build/Dockerfile → one image quadlet/ → the units nginx/<fqdn> → the vhost deploy.ps1 → ships it
  • What: an axum + tokio + sqlx server on PostgreSQL, behind a React 19 + Vite + TypeScript PWA, one repo per app.
  • How: the UI reads and writes the device's store; @platform/sync reconciles it with the server in one POST /api/sync round (Offline & sync).
  • Packaging: build/Dockerfile, quadlet/, nginx/<fqdn>, deploy.ps1, run-local.ps1 and its own e2e/ suite.
  • Pages: CardBox · ClaudeNames · Kouizine · LightFlow · MediaScope · PooPing · TripWise

🖥️ What runs on the server

ODROID-C4 nginx → 127.0.0.1:<port> → containers, one Linux user per service NGINX 443 one vhost per site TLS rate limits SSE · WS kouizine 10050 server postgres kouizine_svc mediascope 10055 server postgres tmdb mediascope_svc · Network=host tripwise 10060 server postgres gmaps osrm tripwise_svc claudenames 10065 server postgres claudenames_svc · WebSockets lightflow 10070 server postgres lightflow_svc · runs the steps cardbox 10075 server postgres cardbox_svc pooping 10080 server postgres pooping_svc platform 10085 server postgres platform_svc · RefApp registry 10090 registry registry_svc · the images cloned apps (Web Servers tab) mkdocs 10000 vaultwarden 10005 planka 10010 immich 10015 calibre 10025 gokapi 10045
  • nginx terminates TLS for every site and proxies to 127.0.0.1:<port>; no container port is reachable from outside.
  • One service, one Linux user: <app>_svc runs its own rootless podman, its units are systemd --user quadlets, and its PostgreSQL is its own.
  • Self-healing: a failed healthcheck kills the container; systemd restarts it with a backoff from 10 s to 300 s and gives up after 6 failures in 30 minutes.

🗺️ Where things live

DEV MACHINE Windows 11 D:\Sources\podman\<repo> sibling clones (the other machine: D:\Source) <repo>\<app>.env gitignored · DEPLOY_REGISTRY=registry.fum-server.fr D:\Sources\podman\cargo-build every native cargo run's build folder Docker Desktop the images · fleet-cargo-* cache mounts %USERPROFILE%\.docker\config.json the registry login D:\Sources\web\mkdocs\docs.fum-server.fr this site ODROID-C4 Debian trixie /media/ssd/git/podman/<app>.git bare repo · post-receive · refs/deploy/* /media/ssd/podman/<app> the clone: quadlet/ nginx/ data/ <app>.env /media/ssd/podman-users/<app> <app>_svc's home: units link into quadlet/ /media/ssd/podman/registry/data the published images /etc/nginx/sites-enabled/<fqdn> a link into the clone's nginx/ /usr/local/bin/podmanctl one script, one sudoers rule /var/log/<app>-deploy.log every deploy · /run/lock/fleet-deploy.lock git push docker push
  • The dev tree's root differs per machine (D:\Sources, D:\Source): every path between repos is relative.
  • Every real .env is gitignored. The server's copy decides how a deploy runs, so a new key goes there before the push that needs it.
  • The server's units and vhost are links into the clone: a git pull updates them.