Atlas Admin Shell: Admin-Konsole, zur Laufzeit komponiert
Microfrontend-Admin-Konsole: Eine zonenlose Angular-21-Shell lädt drei getrennt deployte Apps (IAM, Feature Flags, Audit Logs) zur Laufzeit per Manifest; ein Edge-BFF prüft JWTs. Ohne LLM. Live: echter Login, Mock-Daten.
Gebaut für den Fall, dass mehrere Teams Teile einer Admin-Konsole im eigenen Takt ausliefern: Jede Domäne wird allein deployt, die Shell findet sie zur Laufzeit ohne Neubau, und die Autorisierung greift auf einem Server, nicht im Browser.
- Stack:
- Angular 21 (zoneless, signals) · Native Federation · Nx 23 · Cloudflare Workers · NgRx Signal Store · jose (RS256 JWT) · Nhost Auth (GitHub OAuth) · Tailwind v4 + Style Dictionary · Vitest + Playwright + axe · Storybook 10 · Lighthouse 13
- Zeitraum:
- 2026
- veröffentlicht:
- live:
- atlas.elbaymalik.com
Problem und Rahmenbedingungen
Atlas Admin Shell ist die Art Admin-Konsole, die eine große Organisation über mehrere Teams hinweg betreibt: Identitäts- und Zugriffsverwaltung, Feature Flags und ein Audit-Log, jeweils im Besitz eines anderen Domänenteams. Der schwierige Teil sind nicht die Ansichten. Es ist, jede Domäne für sich bauen, testen und deployen zu lassen, während die Nutzenden eine einzige Konsole sehen, ohne einen Release-Zug durch die Shell. Das Repository nennt als Ergebnis die Architektur, nicht die Features, und seine eigenen Dokumente nennen es auch eine Microfrontend-Steuerebene.
Vier Rahmenbedingungen prägten es. Alle Teilnehmer der Federation nutzen eine exakt gepinnte Angular-Version, sodass sich die Shell und die drei Remotes zur Laufzeit ein Framework teilen. Domänengrenzen erzwingt der Build, nicht eine Konvention: Ein Remote kann kein anderes Remote importieren, nur geteilte Bibliotheken überqueren die Grenze. Die Konsole muss ohne Backend und ohne Identitätsanbieter laufen, damit ein frischer Checkout, ein Review und die Testsuite kein Konto brauchen. Und jede wesentliche Entscheidung ist ein ADR mit einem Verification-Abschnitt aus datierten Belegen; wenn ein Beleg nicht mehr stimmt, sagt der ADR das, statt umgeschrieben zu werden.
Es nutzt kein LLM.
Architektur
Atlas läuft als vier Cloudflare-Worker auf vier Origins: einer für die Shell und einer pro Remote (IAM, Feature Flags, Audit Logs). Die Remotes liegen absichtlich auf fremden Origins, damit die Import Map, die CORS-Header und die Fehlergrenze in der Produktion so arbeiten, wie der Entwurf sie beschreibt.
Beim Start lädt die Shell federation.manifest.json, ungecacht, und initialisiert Native Federation daraus: ES-Module und eine Import Map, keine Bundler-eigene Laufzeit. Jeder Eintrag nennt die Einstiegs-URL, die Route, den Anzeigenamen, das Icon, das exponierte Modul und die erforderliche Rolle eines Remotes, und die Shell baut daraus Routen und Navigation. Eine Domäne hinzuzufügen oder umzuleiten ist eine Änderung an dieser Datei und ein Upload, kein Neubau der Shell. Ein canMatch-Guard prüft die Rolle, bevor ein Remote überhaupt geladen wird. Ein Remote, das nicht lädt, schickt die Nutzenden auf eine Fehlerseite, die der Shell gehört, mit einem erneuten Versuch, der in der Seite funktioniert, statt eines gecachten Fallbacks, den der Router nie wieder versuchen würde.
Jede App ist zonenloses Angular 21 auf Signals. Jedes Remote besitzt seine NgRx Signal Stores, und nur zwei Dinge überqueren eine Grenze: AtlasAuth, der Auth-Dienst, den jedes Remote als dieselbe Instanz injiziert, und ein typisierter Event-Bus, dessen Payloads einen Typ und eine Version tragen und durch Vertragstests festgelegt sind. Remotes importieren einander nie, und eine Lint-Regel von Nx erzwingt das.
Im Worker der Shell geht /api/* an ein Edge-BFF, jeder andere Pfad wird als statische Datei ausgeliefert. Das BFF prüft JWTs mit jose gegen das JWKS des Identitätsanbieters und erzwingt, was ein Browser nicht kann: Flag-Änderungen nur für Admins, den Schutz des letzten Admins, keine Selbstherabstufung, idempotente Schreibzugriffe und einen Audit-Trail, den der Server aus dem geprüften Token schreibt. Ob die Remotes es aufrufen, ist eine Umgebungsangabe: apiMode im Manifest ist mock oder http, und sein auth-Block wählt den Identitätsanbieter, Mock-Personas oder Nhost mit GitHub-Anmeldung. Ohne beides läuft die Konsole mit Mock-Daten und Mock-Anmeldung und braucht überhaupt keinen Server. Die Entscheidungen dahinter stehen im Entscheidungsprotokoll weiter unten, jeweils mit der Alternative, gegen die sie sich durchgesetzt haben.
Behauptungen, die nicht hielten
Das Repository hält seine Korrekturen in den ADRs fest, statt sie umzuschreiben, und sie gehören zu seinen nützlichsten Teilen.
Die Angular-Version: ADR-0002 pinnte Angular 21 LTS, weil Nx, NgRx und Storybook Angular 22 nicht innerhalb ihrer erklärten Peer-Bereiche folgen konnten. Spätestens am 22. Oktober 2026 konnten es alle, und der ADR hatte die alte Ursache monatelang weiter behauptet. Die Entscheidung bleibt, ihr Grund hat sich geändert: Angular 22 hat kein LTS-Tag, nichts darin schließt eine Lücke dieser Codebasis, und es ändert die canMatch-Signatur, auf der die Fehlergrenze aufbaut. Die Migration ist nach Kosten und Nutzen zurückgestellt, nicht blockiert.
Der Manifest-Validator sollte strenger sein als die Laufzeitprüfung der Shell. Als apiMode in den Vertrag kam, lernte es nur die Laufzeitprüfung, also passierte ein Manifest mit einem Tippfehler von einem Buchstaben das Gate und startete eine Konsole mit null Remotes. Ein Test prüft jetzt über erzeugte Mutationen, dass der Validator alles ablehnt, was die Laufzeitprüfung ablehnt, und ein zweiter Test scheitert, wenn die Fixture hinter den Feldern zurückbleibt, die die Laufzeitprüfung liest.
Der erste Lighthouse-Lauf maß die föderierte Route fast genau wie die Shell (LCP 1.446 vs. 1.444 ms), was nicht stimmen kann, wenn eine von beiden ein Remote lädt. Die angemeldete Persona war nie im eigenen Browser-Kontext von Lighthouse angekommen, also leitete die Shell auf eine leere Übersicht um, und der Runner maß diese. Jetzt meldet er nichts, solange der Lauf nicht auf der angeforderten URL gelandet ist und tatsächlich die Chunks eines Remotes geladen hat.
Zwei Schutzmechanismen bestanden jede Suite, während sie fehlten. Das BFF akzeptierte ein Token ohne Ablauf-Claim für immer, weil die Tests nie eines erzeugten. Und nichts im Workspace gestaltete den Tastaturfokus, während axe auf jeder Ansicht bestand, weil axe keine Regel hat, die einen Fokusindikator misst. Beides ist behoben, und jede Korrektur wurde geprüft, indem sie wieder entfernt wurde und der neue Test scheiterte.
Ergebnisse
Die Slices 1 und 2 der vier im Fahrplan sind fertig. Slice 3 (BFF und Deployments) ist bis auf PR-Previews fertig; in Slice 4 (Feinschliff) sind Lighthouse, der axe-Durchgang und die schriftliche Architektur fertig, das Demo-Video und die Migration auf Angular 22 offen. Alle vier Worker wurden am 25. Oktober 2026 neu deployt, und das BFF der Live-Shell lehnt eine nicht authentifizierte Anfrage an /api/flags mit einem JSON-401 ab.
Die Live-Konsole zeigt die Komposition und eine echte Anmeldung, nicht die Sicherheit am Edge. Seit dem 25. Oktober 2026 trägt ihr deploytes Manifest den Nhost-Block, und eine GitHub-Anmeldung gelang dort, von Hand; kein automatisierter Test führt sie aus, und eine Token-Erneuerung in der Produktion ist nicht beobachtet. apiMode steht weiter auf mock, also sind die Daten Mock-Daten, und nichts, was Besuchende tun, erreicht das BFF: Am Edge ist noch keine echte Anfrage geprüft. PR-Previews brauchen einen Edge-Router, der jedes Remote unter einem Origin einhängt, und den gibt es noch nicht; das Demo-Video ist nicht aufgenommen.
Die Zahlen oben stammen aus ADR-0012, gemessen am 6. September 2026 als Median aus drei Desktop-Läufen am Produktions-Build. Die Shell allein erreichte 0,96 bei einem LCP von 1.445 ms, die föderierte Route /flags 0,94 bei 1.629 ms. Ein Remote zur Laufzeit einzubinden kostet rund 318 ms FCP und 184 ms LCP: der Preis des manifestgesteuerten Entwurfs, gemessen statt angenommen. Der Testserver lieferte ohne Komprimierung und ohne Caching aus, die Werte sind also eine Untergrenze auf localhost, keine Zahlen aus der Produktion.
Die Prüfungen laufen lokal. Die Minuten für GitHub Actions bei privaten Repositories waren am 22. Oktober 2026 aufgebraucht, also sind die gehosteten Workflows CI und Deploy deaktiviert, und alle vierzehn Gates laufen mit einem Befehl, pnpm run ci:local: ein Wächter gegen zone.js, Prüfungen von Manifest und Design-Tokens, Lint, Typecheck, Unit- und Vertragstests, die Builds, Wrangler-Prüfungen, eine Bundle-Prüfung, axe über Storybook, End-to-End-Tests der Federation gegen Produktions-Builds, das BFF über HTTP und Lighthouse. Die jüngsten Pull Requests des Repositorys halten fest, dass alle vierzehn bestanden; das ist eingefügte Ausgabe vom Rechner des Autors, keine Prüfung, die GitHub ausgeführt hat.
Lehren
Ein Beleg verfällt. Die Begründung aus ADR-0002 stand monatelang im README, nachdem sie nicht mehr stimmte, in einem Repository, dessen erste Regel lautet, dass eine Behauptung einen Beleg braucht. Jede Prüfung zu datieren machte den Verfall auffindbar; es brachte niemanden dazu, nachzusehen. Belege erneut zu prüfen ist eine Aufgabe, nichts, was man bekommt, weil man sie geschrieben hat.
Das Schweigen eines Gates ist keine Abdeckung. Der Algorithmus-Pin, die Ablaufprüfung, die Origin-Prüfung des Interceptors und der Fokusrahmen bestanden alle jede Suite, während sie kaputt waren oder fehlten, weil kein Test den Fehlerfall erzeugen konnte. Die Korrektur war jedes Mal dieselbe: den Schutz absichtlich brechen und zusehen, wie genau die neuen Tests scheitern und keine anderen.
Planen Sie das Budget für die Architektur ein, die Sie gewählt haben. Laufzeit-Federation kostet Anfragen, bevor die erste Domäne erscheint, und ein Lighthouse-Budget, das streng genug ist, um zu beeindrucken, wäre ein Argument gegen den Entwurf gewesen. Die Shell und eine föderierte Route getrennt zu messen, machte diese Kosten zu einer Zahl.
Gehostete CI zu verlieren ist ein Problem der Disziplin, nicht der Rechenleistung. Die Gates lokal auszuführen, behielt jedes einzelne und führt mehr aus, als die gehostete Pipeline ausgeführt hat, aber nichts hält einen Merge auf, den niemand geprüft hat. Der ADR sagt das, statt etwas anderes vorzugeben.
Zentrale Entscheidungen
01 Native Federation in einem Nx-Monorepo
Ein Nx-Monorepo für die Shell, die drei Remotes und die geteilten Bibliotheken, mit Native Federation (ES-Module und Import Maps auf dem esbuild-Builder von Angular), von Hand verdrahtet, weil Nx dafür keinen Generator hat.
Der eigene Federation-Weg von Nx ist Module Federation, was für Angular den alten webpack-Builder bedeutet. Der Angular-CLI-Workspace ist das unterstützte Zuhause von Native Federation, verliert aber, wofür Nx da ist: den Projektgraphen, Affected-Builds, Caching und per Tags erzwungene Grenzen. Ein Monolith hätte die Architektur entfernt, die das Projekt zeigen soll.
Die Reibung war begrenzt und ist dokumentiert: Die Anfrage nach einem Native-Federation-Generator für Nx war seit Februar 2024 offen, und das Init-Schematic läuft mit nx g, nicht mit ng g. Ein Notausgang stand vorab fest: die Verdrahtung in einem Wegwerf-Workspace der CLI erzeugen und herüberholen (Atlas docs/adr/0001).
verworfen: Nx mit Module Federation über webpack oder rspack · Ein schlichter Angular-CLI-Workspace mit Native Federation · Ein modularer Monolith ohne Federation
02 Überall zonenlos, ohne zone.js im Abhängigkeitsbaum
Die Shell und jedes Remote laufen zonenlos mit Signals; zone.js darf in den installierten Abhängigkeiten gar nicht vorkommen, und ein Gate lässt die Pipeline scheitern, wenn doch.
zone.js ist faktisch ein globales Objekt pro Seite. Auf einer föderierten Seite zieht ein einziges Remote, das davon abhängt, es entweder in die Laufzeit jeder App zurück oder verhält sich neben den zonenlosen unberechenbar; die Mischung ist das Schlechteste aus beiden. Überall zonenlos ergibt ein Modell der Change Detection über jede Grenze hinweg und kleinere Bundles.
Der ADR behält seine eigene Korrektur: Zuerst sagte er voraus, dass Storybooks zone.js-Peer stillschweigend installiert würde. Der Peer ist optional, pnpm installiert nur erforderliche Peers, und der installierte Baum enthält kein zone.js; das Gate prüft das jetzt, statt darauf zu vertrauen (Atlas docs/adr/0003).
verworfen: Jedes Remote wählt selbst zwischen zone.js und zonenlos · zone.js in der ganzen Federation behalten
03 Remotes aus einem Manifest, das ein Deployment-Artefakt ist
Die Shell lädt federation.manifest.json vor dem Start, ungecacht, und baut daraus Routen und Navigation: Jeder Eintrag nennt URL, Route, Anzeigenamen, Icon, exponiertes Modul und erforderliche Rolle eines Remotes. Ein eingechecktes Manifest pro Umgebung wird beim Deployment über die gebaute Shell kopiert, sodass ein Build der Shell jede Umgebung bedient.
Statische Federation deployt die Shell bei jeder Änderung an einem Remote neu. Eine Wahl beim Build macht die Umgebung zu einer Eingabe des Shell-Builds: Nx ersetzt das Assets-Array einer Konfiguration, statt es zu erweitern, und fileReplacements backt das JSON in ein gehashtes Bundle. Eine CI-Vorlage macht Produktfelder zu Pipeline-Ausgabe, die niemand reviewt. Ein Konfigurationsdienst ist das stärkste Endziel, stellt aber eine zweite blockierende Anfrage und einen neuen Ausfallpunkt vor die ganze Konsole; er ist hinter einer Funktion zurückgestellt.
Zum ersten Mal wirklich genutzt am 21. Oktober 2026, als PR #18 die Produktion ohne Neubau von Platzhalter-Domains auf die echten Worker umstellte. Ein Self-Review ergab: Lief die Kopie als eigener Schritt, konnte das Deployment die Shell neu bauen und localhost-URLs ausliefern, ohne dass etwas rot wurde; die Kopie läuft jetzt im Deploy-Target. Ein Validator prüft jedes Manifest, und ein Test stellt sicher, dass er alles ablehnt, was die Laufzeitprüfung der Shell ablehnt (Atlas docs/adr/0004, docs/adr/0010, PR #18).
verworfen: Statische Federation mit fest einkompilierten Remote-URLs · Das Manifest beim Build wählen, per Assets-Mapping oder fileReplacements · Es in der CI aus einer Vorlage erzeugen · Ein Konfigurationsdienst oder KV-Speicher, zur Laufzeit gelesen
04 Das Edge-BFF als Autorisierungsgrenze
Ein Worker auf dem Origin der Shell beantwortet /api/*, alle anderen Pfade bleiben statisch. Er prüft JWTs mit jose gegen das JWKS des Identitätsanbieters (nur RS256, Issuer und Ablauf verpflichtend; keine Audience-Prüfung, weil der Anbieter keinen aud-Claim ausstellt), leitet die Rolle mit derselben geteilten Funktion ab wie der Browser und erzwingt Flag-Änderungen nur für Admins, den Schutz des letzten Admins, keine Selbstherabstufung und idempotente Schreibzugriffe; Audit-Einträge schreibt der Server aus dem geprüften Token.
Ein canMatch-Guard verhindert einen Download, keine direkte Anfrage, und beim Bau des BFF zeigte sich, wie dünn das war: Eine Person mit Leserolle konnte ein Feature Flag umschalten, und nichts lehnte es ab. Derselbe Origin bedeutet keinen CORS-Preflight bei jedem Schreibzugriff und kein neues Feld im Manifest, und eine einzige Rollenfunktion bedeutet, dass Browser und Edge nicht uneins sein können, wer Admin ist.
Die Tests laufen im echten workerd mit lokal erzeugten Schlüsseln. Zwei Schutzmechanismen erwiesen sich als ungetestet oder fehlend: der Algorithmus-Pin und der Ablauf, denn ein Token ohne exp-Claim galt bis zum 21. Oktober 2026 für immer. Ein Konto zu deaktivieren, widerruft dessen Token nicht; das bleibt dem Identitätsanbieter überlassen und ist dokumentiert. Ein Deployment des BFF ist ein Deployment der Shell (Atlas docs/adr/0011).
verworfen: Ein eigener API-Worker auf eigenem Hostnamen · Eine API pro Domäne · Rollen im Browser durchsetzen und die Mock-Daten als Produkt betrachten
05 Auth hinter einer Schnittstelle, Anbieterwahl per Manifest
AtlasAuth ist eine abstrakte Schnittstelle, die die Shell und jedes Remote als eine geteilte Instanz injizieren. Mock-Personas (Admin und Leserolle) sind der Standard; ein Anbieter mit GitHub-Anmeldung, inzwischen Nhost, implementiert dieselbe Schnittstelle und wird über den auth-Block des Manifests gewählt. Kein Block bedeutet den Mock; ein fehlerhafter Block macht das ganze Manifest ungültig.
Supabase zuerst hätte den ersten Slice an die Einrichtung gebunden und der Demo keinen Ausweg gelassen, wenn der Anbieter ausfällt; ein selbst geschriebener Server ist die meiste Arbeit und am wenigsten glaubwürdig. Tokens in Events laden zu Lecks und Race Conditions ein, also erreichen die Remotes Auth nur über die geteilte Instanz. Die Wahl im Manifest hält Umgebungsdaten aus dem Build heraus, wie beim API-Modus.
Der Wechsel war größtenteils ein Drop-in, und der ADR hält fest, wo nicht: Die Login-Signatur trug das Vokabular des Mocks, und ein Token-Getter musste in den geteilten Vertrag aufgenommen werden. Der Interceptor, der das Token auf dem Origin der Shell hält, prüfte bis zum 23. September 2026 nur den Pfad. Kein Remote darf den Identitäts-Client bündeln; eine Build-Prüfung erzwingt das (Atlas docs/adr/0006, PR #22). Im Oktober wechselte der Anbieter von Supabase zu Nhost; das Repository hält fest, was der Wechsel kostete, und sagt, dass es den Grund nicht festhält (Atlas docs/adr/0014). Die Produktion lieferte bis zum 25. Oktober 2026 die Mock-Personas aus; an dem Tag wurden alle vier Worker mit dem Nhost-Block neu deployt, und eine GitHub-Anmeldung in der Live-Konsole gelang, von Hand.
verworfen: Supabase von Anfang an · Ein selbst geschriebener JWT-Server · Tokens per Events an die Remotes weitergeben
06 Lokale Gates, solange das Repository privat ist
tools/ci-local.mjs führt alle vierzehn Gates mit einem Befehl aus, pnpm run ci:local: jedes Projekt statt nur der betroffenen, und nach einem Fehler weiter, um alle zu melden. Die gehosteten Workflows CI und Deploy sind deaktiviert, Deployments laufen vom Rechner des Entwicklers.
Actions-Minuten werden bei privaten Repositories abgerechnet, und dieses verbrauchte sein Monatskontingent am 22. Oktober 2026: Jeder Lauf danach scheiterte, bevor ein Schritt lief. Die Gates sind das Ergebnis, also kam Abschwächen nicht in Frage; Bezahlen bedeutet laufende Kosten für ein Portfolio-Projekt; ein selbst gehosteter Runner ist eine zweite, ständig laufende Maschine, die betrieben und abgesichert werden muss.
Der ADR nennt den Preis: Ein lokales Gate läuft nur, wenn jemand es startet, ein Pull Request trägt eingefügte Ausgabe statt einer Prüfung, die GitHub selbst ausgeführt hat, und die Läufe laufen unter macOS statt unter Linux (Atlas docs/adr/0013).
verworfen: Minuten für GitHub Actions bezahlen · Das Repository öffentlich machen (zurückgestellt, nicht verworfen) · Die Browser-Tests oder Lighthouse streichen, um ins Budget zu passen · Ein selbst gehosteter Runner
Evaluationsergebnisse
| Kennzahl | Wert | Status |
|---|---|---|
| Lighthouse-Performance, Shell-Route / föderierte Route | 0,96 / 0,94 | gemessen / ist die Shell allein, abgemeldet; /flags ist angemeldet und muss das Manifest, drei Remote-Einstiegspunkte von fremden Origins und die Chunks eines Remotes laden, bevor etwas erscheint. Total Blocking Time 0 ms und CLS 0,000 auf beiden. |
| Largest Contentful Paint, Shell-Route → föderierte Route | 1.445 ms → 1.629 ms | gemessen Der Testserver komprimiert nicht und schaltet Caching ab, während die Produktion komprimierte und gecachte Dateien ausliefert; die Zeiten sind also eine Untergrenze auf localhost, keine Werte aus der Produktion. |
| First Contentful Paint, Shell-Route → föderierte Route | 388 ms → 706 ms | gemessen |
| Kosten der Laufzeit-Einbindung eines Remotes, FCP / LCP | +318 ms / +184 ms | gemessen Der Preis dafür, Domänen zur Laufzeit aus einem Manifest zu laden, gemessen statt angenommen. INP ist nicht gemessen: Ein Laborlauf kann ihn nicht liefern, und das Projekt erhebt keine Felddaten. |
Gemessen: Atlas docs/adr/0012, Verification (6. September 2026, Median aus 3 Desktop-Läufen am Produktions-Build, lokal ausgeliefert) · Atlas docs/adr/0012, Verification (6. September 2026, Median aus 3 Desktop-Läufen) · Atlas docs/adr/0012 (die Differenz der beiden Routen oben)
Sicherheit & Fehlerfälle
Risiken
- Eine Rolle aus dem falschen Token-Claim würde jede angemeldete Person zum Admin machen
- Eine Rollenprüfung im Browser verhindert einen Download, keine direkte Anfrage an die API
- Ein Token in einem Dienst, den jedes Remote injizieren kann, könnte an einen anderen Origin gehen
- Ein deaktivierter Admin behält ein gültiges Token, bis es abläuft
Gegenmaßnahmen
- Admin ist eine Rolle nur, wenn der allowed-roles-Claim des Anbieters sie enthält, und die Standardrolle wird nie gelesen; die Live-Konsole läuft zudem mit Mock-Daten, also erreicht nichts, was Besuchende tun, einen Server, und das Edge-BFF beantwortet eine Anfrage ohne Token mit 401
- Das Edge-BFF prüft jeden Schreibzugriff gegen ein geprüftes Token und setzt die Regeln durch, die ein Browser nicht durchsetzen kann, etwa dass der letzte Admin nie entfernt wird
- Der Interceptor hängt das Token nur an /api/-Anfragen auf dem Origin der Shell, und Tests mit fremden Origins sichern das ab
- Dem Identitätsanbieter überlassen (kurze Token-Laufzeiten, Widerruf) und dokumentiert; die Regel zum letzten Admin zählt inaktive Admins nicht als Rückhalt