Micro Frontend

Nutzer setzen & Authentifizierung

Das Micro-Frontend rendert den Ticketshop nativ in deine Seite. Wer der Shop-User ist, kommt aus DEINEM Backend — die Hostseite ist für die Authentifizierung des Nutzers verantwortlich. Ein Session-Token ist Pflicht: ohne ihn bleibt das Micro-Frontend inaktiv. Diese Seite erklärt, warum, und den Server-zu-Server-Session-Flow.

Die Technologie hinter dem Micro-Frontend

Der ADITUS Ticketshop, eingewoben statt teleportiert. Das Micro-Frontend rendert den kompletten Kaufprozess nativ in deine Seite — kein iFrame, kein visueller Fremdkörper. Es ist bewusst schlank und host-nativ gebaut, damit es aussieht und sich anfühlt wie deine eigene Seite, nicht wie ein eingeblendetes Widget.

React 19 — kompatibel bis React 18

Der komplette Ablauf — Events, Artikel, Warenkorb, Registrierung, Zahlung, Abschluss — läuft als ein einziger, durchgängig typsicherer React-Flow. Wir selbst setzen bereits auf React 19, halten die Kompatibilität aber bewusst bis React 18 offen (peerDependency react >=18), damit sich das Micro-Frontend in möglichst viele Host-Seiten einbetten lässt. Der Peer-Bereich ist wörtlich gemeint: Intern nutzt der Adapter ausschließlich den React-18-Feature-Umfang (useState, useEffect, useMemo & Co.) und keine React-19-exklusiven APIs — gebaut und getestet unter React 19, garantiert lauffähig unter React 18. React ist dabei nur eine peerDependency: der Adapter bringt null eigene Runtime-Abhängigkeiten mit. Keine UI-Bibliothek, kein Data-Fetching-Framework, kein Zustands-Toolkit — nur handverlesene React-Hooks. Das Ergebnis ist ein winziges Bundle, das nichts in deine Seite mitschleppt.

Light-DOM statt iFrame

Kein iFrame, kein Shadow-DOM, kein Fremdkörper. Der Shop rendert direkt in das DOM deiner Seite: er erbt deine Typografie, fügt sich in dein Layout ein, ist voll responsiv und barrierefrei — und liest sich für deinen Besucher wie ein nativer Teil deiner Website.

Framework-agnostisch: ein mount(), überall

Ein einziger Aufruf — mount(container, config) — hängt den Shop in ein beliebiges DOM-Element. Ob React, Vue, Angular oder reines HTML: der Adapter ist rahmen-neutral und läuft überall gleich.

Das Styling gehört dir

Jedes Element trägt einen stabilen CSS-Hook (BEM-Klassen aditus-shop__* plus data-state). Du gestaltest frei über dein eigenes CSS — oder setzt mit ein paar --aditus-*-Design-Tokens im Handumdrehen deine Markenfarben, Radien und Abstände. Wahlweise mit sauber isolierten Basis-Styles per CSS @layer, die deine Seite nie überschreiben.

Multilingual, live umschaltbar

DE/EN sind eingebaut. Ein Sprachwechsel wird über handle.update() direkt in die laufende Instanz gespielt — ohne die Journey des Käufers zurückzusetzen. Warenkorb und Fortschritt bleiben erhalten.

Sicher by Design

Ohne gültiges Session-Token ist der Adapter inaktiv — er stellt dann keine einzige Anfrage. Die Session wird server-zu-server mit einem Secret erzeugt; kein Schlüssel landet je im Browser. Alle Aufrufe laufen über einen Proxy, der die Zugangsdaten server-seitig injiziert.

Voraussetzungen an die Host-Umgebung

Auf der Integrationsebene ist das Micro-Frontend bewusst maximal kompatibel — jedes Framework, jede Host-CSS, React 18+. Die unvermeidbaren Fixpunkte sind wenige und harmlos: ein Client im Browser, React vorhanden und eine Session mit whitelisted publicKey.

Was gebraucht wird

  • React 18+ und react-dom 18+ im Host-Bundle — deklariert als peerDependency react >=18; mount() nutzt createRoot aus react-dom/client. Die React-Version bestimmt der Host.
  • Ein Browser mit DOM (client-seitig). Der Adapter mountet in ein echtes DOM-Element (Light-DOM). SSR-Hosts sind ok, solange mount() clientseitig läuft.
  • Ein aktueller Evergreen-Browser — nutzt Standard-Web-APIs (fetch, AbortController, ResizeObserver, CSS @layer), seit ~2022 Baseline. Kein IE.
  • Eine Session plus publicKey — ohne sessionToken bleibt das Micro-Frontend inaktiv. Minte server-zu-server mit dem Secret (Host mit Backend) oder aus dem Browser über den domain-gegateten /shop/embed-session (Host ohne Backend). Die Domain muss auf der Whitelist des publicKey stehen.

Was wir bewusst NICHT voraussetzen

  • Kein bestimmtes Framework — mount(container, config) ist rahmen-neutral: React, Vue, Angular, reines HTML.
  • Kein iFrame, kein Shadow-DOM — es rendert nativ in deine Seite.
  • Keine bestimmte Host-CSS — die Basis-Styles liegen in @layer aditus-shop (jede ungelayerte Host-Regel gewinnt), auf Single-Class-Spezifität, plus Reset-Armor gegen ein Host-* { margin:0; padding:0 }. Komplett abschaltbar mit baseStyles: false.
  • Keine Runtime-Dependencies — nur die React-peerDeps, nichts, was im Host-Bundle kollidiert.
  • Kein bestimmtes Build-Setup außer einem ESM-Import.

Zahlungssicherheit, PCI & CSP

Die Integration von Drittanbieter-Komponenten wie Ticketshops erfordert traditionell eine Abwägung zwischen User Experience, Performance und IT-Sicherheit. Das Micro-Frontend löst diesen Zielkonflikt durch ein hybrides Paradigma: Die Interaktionsstrecke rendert im nativen Light-DOM deiner Seite, der Bezahlvorgang bleibt strikt isoliert — alles Unkritische passiert in deiner Seite, die Zahlung selbst nie.

Keine Zahlungsdaten im Light-DOM

Die komplette Kaufvorbereitung — Event, Tickets, Add-ons, Warenkorb, Registrierung — rendert nativ in deiner Seite. In dem Moment, in dem der Benutzer eine Redirect-Zahlart startet, verlässt der sensible Datenstrom deine Seite vollständig: Eine normale Top-Level-Navigation übergibt an die dedizierte, externe Payment-Page des Zahlungsanbieters, der Rückweg setzt die Journey fort und schließt die Bestellung ab. Karten- oder Bankdaten werden ausschließlich dort eingegeben — das Micro-Frontend rendert, transportiert und speichert in deiner Seite keinerlei Zahlungsdaten.

Minimaler PCI-Scope

Da zu keinem Zeitpunkt Kreditkarten- oder Bankdaten durch deine Host-Anwendung verarbeitet, transportiert oder gespeichert werden, bleibt deine Umgebung im minimalen PCI-DSS-Scope des klassischen Redirect-Modells (typischerweise SAQ A — die verbindliche Einstufung nimmt immer dein Acquirer bzw. QSA vor). Zahlarten ohne Redirect, etwa Rechnung, schließen server-seitig ab — ganz ohne Zahlungsdaten im Browser.

Schlanke Content Security Policy

Zusätzlich zu deiner bestehenden Policy braucht das Micro-Frontend selbst nur zwei Freigaben: connect-src für die ADITUS-API-Origin (jeder Shop-Call läuft darüber) und img-src für die ADITUS-Asset-URLs, falls du Event-Banner oder Artikelbilder anzeigst. Insbesondere braucht es keine script-src- oder frame-src-Einträge für Zahlungsanbieter, denn der Zahlungsschritt ist ein reiner Top-Level-Redirect: In deiner Seite laden nie Zahlungs-Skripte oder -Frames.

Das Beste aus beiden Welten

Klassisch musste man sich entscheiden: iFrame (isoliert, aber starr und ein visueller Fremdkörper) oder voll integrierte API-Lösung (nahtlos, aber dein Security-Team trägt plötzlich den vollen PCI-Scope). Das Micro-Frontend löst dieses Dilemma auf: Die unkritische Interaktion — Stöbern, Warenkorb, Ticketkonfiguration — lebt nativ in deiner Seite; die kritische Interaktion — der Geldeinzug — wird hart abgeschnitten und extern isoliert.

Abgrenzung zu den Marktalternativen

Abgrenzung zum klassischen iFrame

iFrames gelten durch ihre strikte Browser-Isolation als sicher, verhalten sich aber wie isolierte Dokumente: Sie erben weder globale Styles wie Typografie oder responsive Layoutvorgaben, noch sind ihre Inhalte für Crawler zuverlässig indizierbar; für Screenreader bedeuten sie oft einen Medienbruch. Das Micro-Frontend rendert die gesamte Kaufvorbereitung — Artikelauswahl, Warenkorb, Registrierung — als echtes HTML direkt in das DOM deiner Seite und ist damit so barrierefrei und responsiv wie dein eigenes Markup. Die Isolationswirkung eines iFrames entsteht erst im Moment der Zahlung: Der kritische Datenstrom wird per Top-Level-Redirect vollständig an die externe, dedizierte Payment-Page übergeben.

Flexibilität gegenüber reinem Shadow-DOM

Viele Web Components kapseln sich per Shadow-DOM, um CSS-Spezifitätskonflikte zu vermeiden — in der Praxis führt das oft zu unvollständigem Branding, weil globale Styles blockiert werden und Design-Vorgaben mühsam über part-Attribute oder Custom Properties durchgereicht werden müssen. Das Micro-Frontend nutzt stattdessen die native CSS-Kaskade: Die Basis-Styles liegen in @layer aditus-shop, einer bewusst tiefen Ebene — jede ungelayerte CSS-Regel deiner Seite gewinnt automatisch. Dein Corporate Design vererbt sich nativ, ohne Spezifitätskriege mit den UI-Komponenten des Shops.

Ressourcen-Effizienz & Bundle-Size

Monolithische Widgets bringen häufig eigene HTTP-Clients, State-Management und UI-Bibliotheken mit — und zwingen den Browser mitunter, redundante Runtimes parallel zu laden, was die Ladezeit deiner Seite (Core Web Vitals) belastet. Das Micro-Frontend deklariert React und React-DOM ausschließlich als peerDependency und verzichtet vollständig auf eigene Runtime-Abhängigkeiten: Nutzt deine Seite bereits React ab Version 18, klinkt es sich direkt in die vorhandene Runtime ein. Für Nicht-React-Umgebungen kapselt die optionale Web Component die React-Abhängigkeit isoliert im eigenen Bundle, ohne globale Variablen auf deiner Seite zu belegen.

Die Ansätze im Vergleich

KriteriumKlassischer iFrameShadow-DOM-WidgetADITUS Micro-Frontend
SEO & BarrierefreiheitInhalte leben in einem separaten Dokument — für Crawler oft unsichtbar, für Screenreader mühsamer.Isoliert; Auffindbarkeit und Barrierefreiheit brauchen in der Regel Zusatzaufwand.Nativer Teil deines DOMs — indizierbar und barrierefrei wie dein eigenes Markup.
Styling & BrandingStarr: keine CSS-Vererbung, Anpassung nur über postMessage-APIs.Globale Styles greifen nicht; Theming nur über durchgereichte Properties und Parts.Deine CSS gewinnt per @layer-Definition; der Basis-Skin ist überschreibbar oder ganz abschaltbar.
Bundle & PerformanceLädt typischerweise eine komplette zweite App samt Runtime im Frame.Bringt häufig eine eigene Runtime und doppelte Abhängigkeiten mit.Nutzt das React deiner Seite; null eigene Runtime-Dependencies.
Zahlungssicherheit & PCI-ScopeStark isoliert — aber um den Preis von UX, Design und Responsivität.Zahlungsdaten liefen durchs Host-DOM, würden sie dort erfasst.Auswahl im Light-DOM, Zahlung vollständig isoliert auf der externen Payment-Page.

Bezug & Lieferform

Das Micro-Frontend liegt nicht auf dem öffentlichen npm und auf keinem CDN. Es kommt als Quellpaket direkt von deiner ADITUS-Instanz: TypeScript, ESM, vollständig typisiert, ohne eigene Runtime-Abhängigkeiten — dein Bundler (Vite, webpack, Next.js, Nuxt) kompiliert es zusammen mit deiner App wie eigenen Code. Der Import-Name in den Snippets dieser Seite (@workspace/aditus-shop-embed) ist der Paketname unserer Referenz-Integration; das herunterladbare Paket heißt @aditus/shop-embed — die API ist identisch.

Was die Lieferung enthält

Der komplette Adapter als lesbarer TypeScript-Quellcode mit Typdeklarationen für jede Config-Option, jeden Callback und jedes Event. React und react-dom bleiben peerDependencies — deine Seite stellt sie bereit, nichts kollidiert im Bundle, es gibt kein zweites React.

So kommst du an das Paket

Das Paket ist für alle Hosts identisch und lässt sich direkt von dieser Instanz herunterladen, ganz ohne Zugangsdaten: aditus-shop-embed.tgz. Installiere es direkt von dieser URL (npm install akzeptiert eine Tarball-URL — nimm das Link-Ziel auf deiner Instanz, Pfad /api/embed/v1/aditus-shop-embed.tgz) oder entpacke es in dein Repo. Was das Onboarding tatsächlich liefert, ist dein publicKey mit seiner Domain-Whitelist; ohne ihn bleibt das Micro-Frontend inaktiv. Kontakt: api@aditus.dev. Hosts ohne eigenen Build-Schritt (WordPress, statisches HTML) brauchen das Quellpaket gar nicht: Sie nutzen das selbst gehostete Web-Component-Bundle aus der nächsten Sektion.

Web Component: ohne Build-Schritt

Für Seiten ohne eigenen Bundler — WordPress, Typo3, statisches HTML — gibt es das Micro-Frontend zusätzlich als vorgebautes, selbst gehostetes Bundle: Ein Script-Tag registriert das Element <aditus-shop>, React inklusive, kein npm, kein Build. Ausgeliefert wird es von deiner ADITUS-Instanz unter einer versionierten URL (/api/embed/v1/…), nicht über ein öffentliches CDN — für dieses Demo-System ist das https://aditus.dev/api/embed/v1/aditus-shop.js. Die URL ist bewusst öffentlich und braucht keine Zugangsdaten: Das Bundle ist reiner publizierter Code, das eigentliche Gate sind dein Publishable Key mit seiner Domain-Whitelist plus die Session. Dieser Weg ist strikt optional und rein additiv: Hosts mit eigenem Build integrieren weiter das Quellpaket über mount(), genau wie auf dieser Seite dokumentiert — gleiche Journey, gleiche CSS-Hooks, gleiches Theming.

index.html
<!-- 1. Das selbst gehostete Bundle einbinden (einmal pro Seite). -->
<script src="https://ihre-aditus-instanz.de/api/embed/v1/aditus-shop.js" defer></script>

<!-- 2. Das Element platzieren — der Shop rendert nativ an dieser Stelle. -->
<aditus-shop
  public-key="pk_live_ihrkey"
  event-slug="ExperienceDaysv52024"
  culture="de-DE"
></aditus-shop>

<!-- 3. Optional: Journey-Events als ganz normale DOM-Events konsumieren. -->
<script>
  document.querySelector("aditus-shop").addEventListener("cart:update", (e) => {
    console.log("Positionen im Warenkorb:", e.detail.itemCount);
  });
</script>
AttributBedeutung
public-keyDein Publishable Key (Domain-Whitelist greift). Ist nur dieses Attribut gesetzt, mintet das Element seine Browser-Session selbst — der richtige Modus für Seiten ohne Backend.
session-tokenEine von DEINEM Backend gemintete Session (server-zu-server, siehe unten). Ist er gesetzt, mintet das Element nie selbst — deine Seite besitzt den Session-Lebenszyklus.
api-baseBasis-URL der ADITUS-API. Default ist die Origin, von der das Bundle-Script geladen wurde — normalerweise setzt du das nie.
event-id / event-slugPinnt die Journey auf ein Event (überspringt die Event-Auswahl). Bevorzugt den sprechenden Slug.
cultureShop-Sprache/-Locale, z. B. de-DE oder en-GB. Eine Attribut-Änderung re-lokalisiert an Ort und Stelle — die Journey des Benutzers bleibt erhalten.
start-phase / article-layout / event-layout / article-select-mode / show-headerDie flachen Darstellungsoptionen, gleiche Werte wie in der mount()-Config (articles, cards, quantity, …).
base-stylesAuf "false" gesetzt entfällt der neutrale Default-Skin und du stylst jeden Hook selbst.

Session & Selbstheilung

Die Session-Regeln dieser Seite gelten unverändert. Mit Backend mintest du server-zu-server und setzt session-token — das Element mintet dann nie selbst. Ohne Backend setzt du nur public-key: Das Element mintet eine Browser-Session über den Domain-gegateten Endpunkt und mintet bei Ablauf transparent nach, der Benutzer landet nie in einer Sackgasse. Light DOM wie immer: kein iFrame, kein Shadow DOM — dein CSS erreicht jeden dokumentierten Hook.

Events & Versionierung

Der Event-Bus erscheint als ganz normale DOM-CustomEvents auf dem Element (bubbelnd, Payload in event.detail) — cart:update, checkout:complete und der analytics-Spiegel, gleiche Namen und Payloads wie bei handle.on(). Die URL trägt die Major-Version: v1 erhält kompatible Updates an Ort und Stelle; ein Breaking Change erscheint als /embed/v2/, damit sich unter deiner Seite nichts unangekündigt ändert. Alles jenseits der flachen Attribute — Theme-Objekte, Callbacks wie onUserRequired, Card-Content — bleibt bewusst dem Quellpaket vorbehalten.

Attribute, keine Properties

Das Element wird ausschließlich über HTML-Attribute konfiguriert — und Attribute sind immer Strings. base-styles="false" ist der wörtliche String "false"; das Element parst ihn, JavaScript-Properties setzt du nie. Frameworks binden Namen mit Bindestrich wie public-key ohnehin als Attribut, es braucht also keine besondere Binding-Syntax. Alles, was sich nicht als flacher String ausdrücken lässt — Theme-Objekte, Callbacks, Card-Content — ist bewusst kein Attribut: Dafür ist das Quellpaket da.

Updates ohne Remount

Eine Attribut-Änderung am lebenden Element aktualisiert die laufende Instanz an Ort und Stelle — genau wie handle.update() im Quellpaket. Darstellungs-Attribute (culture, Layouts) erhalten die Journey des Benutzers; nur eine Änderung der Session-Identität (public-key, session-token, api-base) etabliert die Session neu. Wird das Element aus dem DOM entfernt, unmountet der Shop sauber — inklusive aller Listener.

SSR-Hosts: Next.js & Nuxt

Das Micro-Frontend ist bewusst client-only. Sein Inhalt ist live und session-gebunden — Verfügbarkeiten, Preise und der Warenkorb des Käufers existieren nur für eine authentifizierte Session zum Abrufzeitpunkt. Es gibt also nichts Sinnvolles, das ein Host-Server vorrendern könnte. mount() erzeugt einen frischen client-seitigen React-Root (createRoot); server-gerendertes Markup im Container wird nicht hydriert. Genau das macht die SSR-Integration einfach und vorhersehbar: auf dem Server einen Platzhalter rendern, im Client mounten, den Platz reservieren — nichts springt.

ticketshop-section.tsx
// Next.js (App Router) — das Micro-Frontend ist bewusst client-only.
"use client";
import { useEffect, useRef } from "react";
import { mount, type ShopHandle } from "@workspace/aditus-shop-embed";

export function TicketshopSection({ sessionToken }: { sessionToken: string }) {
  const el = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!el.current) return;
    const handle: ShopHandle = mount(el.current, {
      publicKey: "pk_deine_seite",
      sessionToken, // server-seitig gemintet, als Prop an die Client-Komponente
    });
    return () => handle(); // Unmount beim Routenwechsel
  }, [sessionToken]);

  // Server und Client rendern DENSELBEN Platzhalter — kein Hydration-Mismatch.
  // min-height reserviert den Platz, damit die Seite beim Mount nicht springt (CLS).
  return <div id="aditus-shop" ref={el} style={{ minHeight: 560 }} />;
}

Layout-Sprünge vermeiden (CLS)

Gib dem Mount-Container eine min-height, die grob der ersten Shop-Ansicht entspricht, und rendere darin optional ein eigenes Skeleton — Server und Client erzeugen denselben Platzhalter, es gibt keinen Hydration-Mismatch. Der Adapter rendert direkt nach dem Mount und ersetzt den Platzhalter in einem Paint. Versuche kein hydrateRoot auf dem Container: Es gibt keinen server-gerenderten Shop-Baum, an den sich React heften könnte.

Die Session im SSR-Host

SSR-Hosts haben einen natürlichen Vorteil: Dieselbe Server-Runtime, die die Seite rendert, kann auch die Session minten — server-zu-server mit dem Secret, genau wie im Mint-Flow weiter unten (Next.js: Route Handler oder Server Component; Nuxt: Server-Route). Reiche den entstandenen opaken Token als Prop oder Payload an die Client-Komponente. Das Secret taucht nie im Client-Code auf, der Token bleibt im Speicher.

Next.js im Detail

Markiere die mountende Komponente mit "use client" und mounte im useEffect (siehe Snippet). Im Pages Router erreicht next/dynamic mit ssr: false dasselbe. Gib das Handle als Cleanup aus dem Effect zurück, damit client-seitige Routenwechsel sauber unmounten. Ein Sprachwechsel an der laufenden Instanz läuft über handle.update(), ohne die Journey zurückzusetzen; ein neuer Session-Token mountet neu (wie im Snippet) — auch das übersteht der Warenkorb, weil das Micro-Frontend die Journey beim Mount wiederherstellt.

Nuxt / Vue im Detail

Kapsle das Mount-Ziel in <ClientOnly> oder mounte in onMounted — der Adapter braucht ein echtes DOM-Element im Browser. Denk an die Host-Bundle-Voraussetzung: react und react-dom 18+ müssen als Dependencies deiner Nuxt-App installiert sein; der Shop rendert in seinen eigenen Container und funkt Vues Virtual-DOM nicht dazwischen.

Das Micro-Frontend braucht eine Session

Ohne Token

Ohne Session-Token — das Micro-Frontend bleibt inaktiv

Ohne Session-Token rendert das Micro-Frontend nur einen neutralen Hinweis und startet KEINE API-Calls. Es kann kein Warenkorb gefüllt werden, keine Registrierung, kein Checkout. Eine Session ist Pflicht, bevor der Shop aktiv wird — sie muss aber nicht an einen bekannten Nutzer gebunden sein: Eine Gast-Session genügt zum Browsen und Füllen des Warenkorbs, der echte Nutzer kann später beim Checkout festgelegt werden (siehe den Optional-User-Flow unten).

mount.ts
import { mount } from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// Ohne sessionToken bleibt das Micro-Frontend inaktiv: es zeigt nur einen
// neutralen Hinweis und startet KEINE API-Calls — kein Warenkorb,
// keine Registrierung, kein Checkout. Erst eine Session aktiviert den Shop.
mount(el, {
  publicKey: "pk_deine_seite",
  // sessionToken fehlt -> Shop wird nicht aktiviert.
});
Pro-Host-User

Session-Token — ein konkreter User

Dein Backend mintet einen kurzlebigen Token, der an genau einen Shop-User gebunden ist. Das Micro-Frontend agiert dann als dieser User: dessen Warenkorb, dessen Daten. Der Token ist opak, ablaufend, single-user und an deinen publicKey gebunden — und dein Secret erreicht nie den Browser.

mount.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@workspace/aditus-shop-embed";

// getElementById kann null sein — in TypeScript sauber prüfen statt "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// AditusShopConfig typisiert alle Optionen — Autovervollständigung inklusive.
const config: AditusShopConfig = {
  publicKey: "pk_deine_seite", // Pflicht, sobald eine Session im Spiel ist
  sessionToken,                // vom Backend gemintet, nur im Speicher halten
};

const handle: ShopHandle = mount(el, config);

// Das Micro-Frontend sendet bei jedem Shop-Call den Header X-Aditus-Session.
// Beim Logout die Session serverseitig widerrufen und neu mounten/aktualisieren.

Ein Seiten-Reload verliert die Journey nicht. Das Micro-Frontend merkt sich Warenkorb und Schritt lokal, lädt den Live-Warenkorb beim Mount vom Server nach und macht genau dort weiter, wo der Käufer aufgehört hat — auch wenn deine Seite bei jedem Laden einen frischen Session-Token mintet. Ist der Warenkorb serverseitig abgelaufen, startet die Journey einfach neu. Deine Seite muss dafür nichts tun.

Optional: anonym browsen, beim Checkout anmelden

Du musst nicht von Anfang an wissen, wer der Käufer ist. Mounte mit einer Gast-Session, und das Micro-Frontend lässt einen anonymen Besucher Events durchstöbern, Artikel wählen und den Warenkorb füllen. Erst wenn er den Warenkorb verlässt — Richtung Registrierung und Checkout — bittet es dich, den echten Nutzer festzulegen, über den Callback onUserRequired. Der restliche Ablauf läuft dann mit erhaltenem Warenkorb weiter.

  1. 1

    Mit einer Gast-Session mounten

    Ein Session-Token ist weiterhin nötig, um den Shop zu aktivieren — es kann aber eine Gast-Session sein, ohne bekannten Nutzer. Der Besucher browst und füllt den Warenkorb darunter.

  2. 2

    Das Verlassen des Warenkorbs löst onUserRequired aus

    Sobald der Besucher den Warenkorb verlässt, ruft das Micro-Frontend deinen Callback einmal auf und zeigt einen neutralen Wartezustand. Authentifiziere den Nutzer (Login/SSO) und minte dann eine frische, nutzergebundene Session genau wie die erste: server-zu-server mit deinem Secret. Auch das spätere Festlegen des Nutzers läuft über diesen Machine-to-Machine-Mint — das Secret erreicht nie den Browser und der Nutzer wird nie im Client-Code gesetzt. Dein Callback reicht nur den entstandenen opaken Token weiter. Entscheidend: Dein Backend leitet diese Identität aus SEINER eigenen authentifizierten Session ab (Cookie/JWT); es darf E-Mail oder User-ID niemals aus dem Browser entgegennehmen, sonst könnte ein böswilliger Besucher eine fremde Session anfordern und über den Prefill deren personenbezogene Daten lesen.

  3. 3

    Gib den neuen Token zurück — der Warenkorb wandert mit

    Gib den frischen Session-Token zurück; das Micro-Frontend übernimmt ihn und baut den Warenkorb unter dem identifizierten Nutzer neu auf, samt gewählter Artikel. Danach geht es zur Registrierung. Gib null zurück, um abzubrechen — der Besucher bleibt einfach im Warenkorb, ohne Fehler.

  4. 4

    Ohne onUserRequired: fixer Nutzer

    Lässt du den Callback weg, ändert sich nichts am klassischen Ablauf: Mounte mit einer nutzergebundenen Session, und das Micro-Frontend agiert vom ersten Schritt an als dieser Nutzer.

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// Ein GAST-Session-Token reicht zum Mounten (server-zu-server gemintet, z. B.
// für einen anonymen Besucher). Der Besucher darf browsen UND den Warenkorb
// füllen, ohne dass ein konkreter Nutzer feststeht.
const config: AditusShopConfig = {
  publicKey: "pk_deine_seite",
  sessionToken: guestSessionToken,

  // Wird EINMAL aufgerufen, sobald der Besucher den Warenkorb Richtung
  // Registrierung verlässt. Hier authentifizierst du den Nutzer (Login/SSO)
  // und mintest server-zu-server eine NEUE, nutzergebundene Session.
  // Gib den frischen Token zurück -> das Micro-Frontend übernimmt ihn und baut
  // den Warenkorb unter dem identifizierten Nutzer neu auf (Artikel bleiben
  // erhalten). Gib null zurück, um abzubrechen -> der Besucher bleibt im
  // Warenkorb, ohne Fehler.
  onUserRequired: async () => {
    // Ruft NUR dein Backend auf – ohne Identität im Payload. Dein Backend liest
    // den eingeloggten Nutzer aus SEINER eigenen Session (Cookie/JWT) und mintet
    // server-seitig für GENAU diese Identität. Niemals die E-Mail aus dem Browser
    // übergeben – sonst könnte ein Angreifer hier eine fremde Adresse einsetzen
    // und über den Prefill an deren personenbezogene Daten gelangen.
    const res = await fetch("/api/shop/my-session", { method: "POST" });
    if (res.status === 401) return null;        // nicht eingeloggt -> im Warenkorb bleiben
    const { sessionToken } = (await res.json()) as { sessionToken: string };
    return sessionToken;
  },
};

const handle: ShopHandle = mount(el, config);

Die Hostseite authentifiziert den Nutzer

ADITUS authentifiziert deine Endnutzer nicht. Deine Seite ist die alleinige Quelle der Wahrheit, wer der Nutzer ist — du betreibst dein eigenes Login, SSO oder deine Session. ADITUS vertraut lediglich einem Session-Token, das dein Backend mit einem Secret-Key mintet. Die Vertrauenskette lautet: dein Auth beweist die Identität, dein secret-geminteter Token verbürgt sie, das Micro-Frontend präsentiert sie.

  1. 1

    Deine Seite authentifiziert den Nutzer

    Login, SSO, Mitglieder-Session — komplett deine eigene. ADITUS ist hier nicht beteiligt.

  2. 2

    Dein Backend mintet eine Session

    Server-zu-Server mit dem Secret-Key. Du übergibst, wer das Micro-Frontend sein soll (E-Mail), und bindest es an deinen publicKey.

  3. 3

    Die Seite mountet das Micro-Frontend

    Der opake Token wird an den Browser gereicht und als sessionToken an mount() übergeben. Der Secret bleibt auf deinem Server.

  4. 4

    Jeder Call trägt die Session

    Das Micro-Frontend sendet bei jeder Anfrage X-Aditus-Session; der Proxy löst den Warenkorb-User aus der Session auf.

  5. 5

    Kein Token → Micro-Frontend bleibt inaktiv

    Ohne Session wird das Micro-Frontend nie aktiv: es zeigt einen neutralen Hinweis und macht keine Calls — kein Warenkorb, kein Checkout. Minte eine Session, um es einzuschalten.

1. Auf deinem Backend minten

server.ts
// SERVER-SEITE deines Hosts — der Secret-Key verlässt NIE den Browser.
// Dein Backend hat den Nutzer bereits selbst authentifiziert (Login/SSO/Session).
// WICHTIG: user stammt aus DIESER Server-Session, nie aus dem Request-Body des
// Browsers — sonst könnte ein Angreifer eine fremde E-Mail unterschieben.
const res = await fetch("https://<dein-host>/api/shop/session", {
  method: "POST",
  headers: {
    // Secret-Key, nur server-seitig. Mintet im Auftrag deiner Seite.
    Authorization: `Bearer ${process.env.ADITUS_EMBED_SESSION_SECRET}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    publicKey: "pk_deine_seite",      // bindet die Session an deinen Key (Pflicht)
    email: user.email,                 // wer das Micro-Frontend sein soll (Pflicht)
    externalUserId: user.id,           // optionale Metadaten (deine User-ID)
    ttlSeconds: 1200,                  // optional: Lebensdauer (Default 20 min, max 1 h)
  }),
});

// Antwort-Typ deiner Wahl — sessionToken an den Browser reichen (inline oder fetch).
const { sessionToken }: { sessionToken: string; expiresAt: number } = await res.json();

2. Mit dem Token mounten

page.ts
import {
  mount,
  type ShopHandle,
  type AditusShopConfig,
} from "@workspace/aditus-shop-embed";

// getElementById kann null sein — in TypeScript sauber prüfen statt "!".
const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

// AditusShopConfig typisiert alle Optionen — Autovervollständigung inklusive.
const config: AditusShopConfig = {
  publicKey: "pk_deine_seite", // Pflicht, sobald eine Session im Spiel ist
  sessionToken,                // vom Backend gemintet, nur im Speicher halten
};

const handle: ShopHandle = mount(el, config);

// Das Micro-Frontend sendet bei jedem Shop-Call den Header X-Aditus-Session.
// Beim Logout die Session serverseitig widerrufen und neu mounten/aktualisieren.

3. Beim Logout widerrufen

server.ts
// SERVER-SEITE — z. B. beim Logout deines Nutzers.
await fetch(`https://<dein-host>/api/shop/session/${sessionToken}`, {
  method: "DELETE",
  headers: { Authorization: `Bearer ${process.env.ADITUS_EMBED_SESSION_SECRET}` },
});
// { revoked: true }

Die fertige Bestellung kommt zurück an deine Seite

Wenn ein Ticketkauf abgeschlossen ist, brauchst du keine separate Order-Abfrage. Weil das Micro-Frontend nativ in deiner Seite läuft (Light-DOM, kein iFrame), reicht es die fertige Bestellung direkt per onComplete-Callback an deinen Code zurück — ein direkter JS-Aufruf, kein postMessage. Das Micro-Frontend hat die Bestellung bereits für dich aufgelöst: du bekommst die Bestellnummer und die Ticket-Links, nicht nur eine ID.

mount.ts
import {
  mount,
  type ShopHandle,
  type CompletedOrder,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
  // Wird GENAU EINMAL aufgerufen, sobald die Bestellung platziert ist.
  // Das Micro-Frontend hat die Bestellung bereits für dich aufgelöst — du bekommst
  // die aufgelöste Bestellung, nicht nur eine ID.
  onComplete: (order: CompletedOrder) => {
    order.number;   // Bestellnummer (Order-ID als Fallback) — dein Identifier
    order.status;   // Status der Bestellung
    order.buyer;    // Käufer (Name / E-Mail), falls vorhanden
    order.tickets;  // [{ name, links: [{ kind: "pdf" | "apple" | "google", url }] }]

    // -> Order-ID speichern, Bestätigung zeigen, DOI / Newsletter anstoßen ...
  },
  // Wird bei jedem nicht behebbaren Fehler im Ablauf aufgerufen.
  onError: (err: Error) => {
    console.error("Shop-Fehler:", err.message);
  },
});

Was du bekommst

onComplete feuert einmal pro erfolgreichem Checkout mit der aufgelösten Bestellung: number (die Order-ID / Bestellnummer — dein Identifier), status, buyer (Name & E-Mail, falls vorhanden) und tickets — jedes Ticket trägt typisierte Links mit kind „pdf“, „apple“ oder „google“. onError feuert bei jedem nicht behebbaren Fehler im Ablauf.

Zahlarten mit Redirect

Zahlarten, die auf der Seite bleiben (Rechnung, Vorkasse, Inline-Karte), feuern onComplete sofort. Eine Zahlart, die den Browser auf eine externe Bezahlseite schickt (z. B. Saferpay), übernimmt das Micro-Frontend komplett: Es leitet die Rücksprung-URLs aus der Seite ab, auf der es läuft, schickt den Käufer hinaus und schließt die Bestellung beim Rücksprung selbst ab — inklusive onComplete, ganz ohne Code auf deiner Seite. Nur wenn der Rücksprung auf einer ANDEREN Seite landen soll, überschreibst du redirectUrls.successUrl / cancelUrl / errorUrl.

GA4- / E-Commerce-Events (optional)

Das Micro-Frontend kann den Einkaufs-Funnel an deine Analytics melden — aber nur, wenn du es darum bittest. Es bündelt kein gtag, kein GTM und kein GA-SDK, lädt nichts, setzt keine Cookies und sendet von sich aus nichts. Du ergänzt einen optionalen Callback, onEvent, und bekommst typisierte Events, die bereits im GA4-Enhanced-Ecommerce-Schema von Google geformt sind. Lässt du onEvent weg, ändert sich nichts. Welches Tool du fütterst und ob du senden darfst (Consent), bleibt komplett bei dir.

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopAnalyticsEvent,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
  // OPTIONAL. Lässt du onEvent weg, ändert sich nichts: das Micro-Frontend
  // sendet nichts, lädt kein gtag/GTM/GA-SDK und setzt keine Cookies.
  // Tool, Consent und Mapping bleiben komplett bei dir — hier GA4 via gtag.
  onEvent: (event: ShopAnalyticsEvent) => {
    // Consent liegt bei dir: erst senden, wenn der Nutzer zugestimmt hat.
    if (!hasAnalyticsConsent()) return;
    // Die Events sind bereits im GA4-Schema (name + items/value/currency ...),
    // also 1:1 an gtag durchreichen.
    const { name, ...params } = event;
    window.gtag?.("event", name, params);
  },
});

Bereits im GA4-Format

Jedes Event trägt einen GA4-Event-Namen plus seine Standard-Parameter: items (mit item_id, item_name, price, quantity, item_category und — wo zutreffend — discount und coupon), value und currency; add_payment_info ergänzt payment_type, purchase ergänzt transaction_id. Nimm name per Destructuring heraus und reiche den Rest direkt an gtag("event", name, params) — kein Umbauen nötig.

Consent bleibt bei dir

onEvent ist einfach eine ganz normale JavaScript-Funktion in deiner Seite, die wir direkt aufrufen. Das Micro-Frontend selbst sendet keine Analytics irgendwohin — nichts verlässt die Seite, bis dein Handler sendet. Dein Consent-Management behält also die volle Kontrolle: koppel das Weiterreichen an den Consent, route in den dataLayer von GTM statt an gtag, batche oder verwirf Events ganz. Ein Fehler, den dein Handler wirft, wird abgefangen und isoliert und bricht den Benutzer-Flow nie — halte den Handler leichtgewichtig (ein gtag-/dataLayer-Push), da er inline läuft.

EventFeuert, wenn
view_item_listDer Benutzer sieht die Artikelliste (feuert einmal pro angezeigtem Set).
add_to_cartEin Artikel oder Add-on wird in den Warenkorb gelegt.
remove_from_cartEine Warenkorb-Position oder ein Add-on wird entfernt.
view_cartDie Warenkorb-Ansicht wird angezeigt (einmal pro Warenkorb).
begin_checkoutDer Benutzer verlässt den Warenkorb Richtung Registrierung / Checkout.
add_payment_infoEine Zahlart wird gewählt (enthält payment_type).
purchaseDie Bestellung ist platziert (enthält transaction_id, value, items).

Event-Bus: handle.on / handle.off (optional)

Neben den Config-Callbacks trägt das von mount() zurückgegebene Handle einen kleinen, komplett optionalen Event-Bus. Du abonnierst mit handle.on(event, listener) — der Aufruf gibt die passende Abmeldefunktion zurück — und meldest dich mit handle.off(event, listener) ab. Rufst du on() nie auf, ändert sich nichts: Der Bus bringt keine Abhängigkeiten mit, sendet nichts und kostet nichts. Er ist reine Beobachtung für deine Seite — ein Mini-Cart-Badge, ein Bestätigungsbanner, dein eigenes Logging — und ergänzt die Callbacks, statt sie zu ersetzen.

mount.ts
import {
  mount,
  type ShopHandle,
  type ShopCartUpdate,
} from "@workspace/aditus-shop-embed";

const el = document.getElementById("aditus-shop");
if (!el) throw new Error("Mount-Ziel #aditus-shop nicht gefunden");

const handle: ShopHandle = mount(el, {
  publicKey: "pk_deine_seite",
  sessionToken,
});

// OPTIONAL. Ohne on() verhält sich das Micro-Frontend exakt wie bisher —
// der Bus ist reine Beobachtung, kein Event ist Pflicht.
// on() liefert die Abmeldefunktion zurück; alternativ handle.off(name, fn).
const offCart = handle.on("cart:update", (cart: ShopCartUpdate) => {
  // Bei jeder Warenkorb-Änderung: Anzahl, Summe, Positionen (GA4-Item-Form).
  updateMiniCartBadge(cart.itemCount); // z. B. Badge im Seiten-Header
});

handle.on("checkout:complete", ({ order }) => {
  // Bestellung platziert — dieselbe aufgelöste Bestellung wie in onComplete.
  showConfirmationBanner(order.number);
});

handle.on("analytics", (event) => {
  // Spiegel des GA4-Funnels (identische Payloads wie config.onEvent).
  console.debug("Funnel:", event.name);
});

// Später gezielt abmelden — der Rest bleibt aktiv:
offCart();
EventFeuert, wenn
cart:updateDer Warenkorb ändert sich: angelegt, Position hinzugefügt oder entfernt, oder geleert. Enthält cartId, itemCount, value, currency und die Positionen als GA4-geformte items.
checkout:completeDie Bestellung ist platziert — derselbe Moment und dasselbe aufgelöste Order-Objekt wie config.onComplete.
analyticsSpiegel des GA4-Funnels oben: jede onEvent-Payload wird unverändert auch auf dem Bus emittiert.

Listener überleben update()

Abos hängen am Handle, nicht an einem Render: handle.update({ ... }) rendert das Micro-Frontend neu, lässt aber jeden Listener angeschlossen. Der Unmount via handle() löst alle Listener automatisch — off() (oder die zurückgegebene Abmeldefunktion) brauchst du nur, wenn du aufhören willst zuzuhören, während der Shop weiterläuft.

Isoliert, nie blockierend

Ein Listener, der einen Fehler wirft, wird abgefangen und isoliert — er bricht nie den Benutzer-Flow und stört keine anderen Listener. Das Event „analytics“ spiegelt den GA4-Funnel mit Payloads identisch zu config.onEvent; du kannst den Funnel also über den Bus, den Callback oder beides konsumieren — mit derselben Consent-Regel: Nichts verlässt die Seite, bis dein Code sendet.

publicKey vs. sessionToken

Halte die beiden auseinander. Sie beantworten verschiedene Fragen und werden kombiniert, nicht ausgetauscht.

publicKey — Origin-Vertrauen

Publishable. Er steckt in deinem Client-Code, ist also KEIN Secret und gewährt für sich genommen nichts. Das Backend erzwingt eine Domain-Whitelist pro Key: ein Key funktioniert nur von seinen registrierten Domains. Er beantwortet „welche Seite ist das?“

sessionToken — Identität

Secret-geminted auf deinem Backend, opak und kurzlebig. Er beantwortet „wer ist der Nutzer?“ Eine Session ist an den publicKey gebunden, für den sie gemintet wurde: eine Session-Anfrage muss diesen Key mitsenden, und eine Abweichung wird abgelehnt (403).

Sicherheitsregeln

  • Der Secret-Key lebt nur auf deinem Server — nie in Client-Code, Bundles oder an den Browser ausgelieferten Env-Dateien.
  • Halte den Token nur im Speicher. Persistiere ihn nicht in localStorage; erneuere ihn durch Neu-Minten.
  • Tokens sind kurzlebig (Default 20 Minuten, max 1 Stunde) und single-user.
  • Widerrufe die Session beim Logout, damit ein geleakter Token nicht erneut verwendet werden kann.
  • Kombiniere die Session immer mit ihrem publicKey — ein fehlender oder abweichender Key wird abgelehnt (403).
  • Eine fehlerhafte oder abgelaufene Session ist ein hartes 401 — niemals ein stiller Rückfall auf den Demo-User.

Endpunkt- & Antwort-Referenz

POST/api/shop/sessionminten — Bearer-Secret
DELETE/api/shop/session/:tokenwiderrufen — Bearer-Secret

In dieser Demo läuft das Minting über den Proxy unter den obigen Pfaden. In Produktion wird die Session im ADITUS-Core gemintet. Fehler sind explizit:

StatusCodeBedeutung
503session_not_configuredMinting ist deaktiviert, weil der Server-Secret (ADITUS_EMBED_SESSION_SECRET) nicht gesetzt ist.
401unauthorizedDer Bearer-Secret beim Mint-/Revoke-Aufruf fehlt oder ist falsch.
400invalid_public_keyDer publicKey ist fehlerhaft oder kein registrierter Key.
400invalid_emailDie E-Mail des zu mintenden Shop-Users fehlt oder ist ungültig.
401invalid_session / session_expiredEin Shop-Call trug ein fehlerhaftes oder abgelaufenes X-Aditus-Session — niemals ein stiller Rückfall auf den Demo-User.
403session_requires_public_keyEine Session wurde ohne den Header X-Aditus-Public-Key gesendet (Origin-Pinning ist Pflicht, sobald eine Session im Spiel ist).
403session_key_mismatchDer X-Aditus-Public-Key passt nicht zu dem Key, für den die Session gemintet wurde.

Jetzt zu deinem Look

Die Identität steht — jetzt branden wir das Micro-Frontend. Das Styling-Tool erzeugt eine fertige Theme-Config zum Einfügen und erklärt jeden Parameter, mit Live-Vorschau.

Zum Styling-Tool