Skip to content
Micro Frontend

Setting the user & authentication

The micro-frontend renders the ticketshop natively into your page. Who the user is comes from YOUR backend — the host page is responsible for authenticating the user. A session token is mandatory: without one the micro-frontend stays inactive. This page explains why and the server-to-server session flow.

The technology behind the micro-frontend

The ADITUS ticketshop, woven in instead of teleported. The micro-frontend renders the complete purchase flow natively into your page — no iframe, no visual foreign body. It is built lean and host-native, so it looks and feels like your own site, not an embedded widget.

React 19 — compatible down to React 18

The complete flow — events, articles, cart, registration, payment, completion — runs as a single, fully type-safe React flow. We already run on React 19 ourselves, but deliberately keep compatibility open down to React 18 (peerDependency react >=18) so the micro-frontend embeds into as many host pages as possible. That peer range is meant literally: internally the adapter uses only the React 18 feature set (useState, useEffect, useMemo & co.) and no React-19-exclusive APIs — built and tested under React 19, guaranteed to run under React 18. React is only a peerDependency: the adapter brings zero runtime dependencies of its own. No UI library, no data-fetching framework, no state toolkit — just hand-picked React hooks. The result is a tiny bundle with nothing that drags along in your page.

Light DOM instead of iframe

No iframe, no shadow DOM, no foreign body. The shop renders directly into your page's DOM: it inherits your typography, fits into your layout, is fully responsive and accessible — and reads to your visitor as a native part of your website.

Framework-agnostic: one mount(), everywhere

A single call — mount(container, config) — attaches the shop to any DOM element. Whether React, Vue, Angular or plain HTML: the adapter is framework-neutral and runs the same everywhere.

The styling is yours

Every element carries a stable CSS hook (BEM classes aditus-shop__* plus data-state). You style freely via your own CSS — or set your brand colors, radii and spacing in no time with a handful of --aditus-* design tokens. Optionally with cleanly isolated base styles via CSS @layer that never override your page.

Multilingual, switchable live

DE/EN are built in. A language switch is pushed straight into the running instance via handle.update() — without resetting the user's journey. Cart and progress are preserved.

Secure by design

Without a valid session token the adapter is inactive — it then makes not a single request. The session is created server-to-server with a secret; no key ever reaches the browser. All calls run through a proxy that injects the credentials server-side.

Host environment requirements

On the integration layer the micro-frontend is deliberately maximally compatible — any framework, any host CSS, React 18+. The unavoidable fixed points are few and harmless: a client in the browser, React present, and a session with a whitelisted publicKey.

What is required

  • React 18+ and react-dom 18+ in the host bundle — declared as peerDependency react >=18; mount() uses createRoot from react-dom/client. The host decides the React version.
  • A browser with a DOM (client-side). The adapter mounts into a real DOM element (light DOM). SSR hosts are fine as long as mount() runs on the client.
  • A current evergreen browser — uses standard web APIs (fetch, AbortController, ResizeObserver, CSS @layer), baseline since ~2022. No IE.
  • A session plus publicKey — without a sessionToken the micro-frontend stays inactive. Mint server-to-server with the secret (host with backend) or from the browser via the domain-gated /shop/embed-session (backend-less host). The domain must be on the publicKey's whitelist.

What we deliberately do NOT require

  • No particular framework — mount(container, config) is framework-neutral: React, Vue, Angular, plain HTML.
  • No iframe, no shadow DOM — it renders natively into your page.
  • No particular host CSS — base styles live in @layer aditus-shop (any unlayered host rule wins), at single-class specificity, plus reset-armor against a host * { margin:0; padding:0 }. Fully switchable off with baseStyles: false.
  • No runtime dependencies — only the React peerDeps, nothing that collides in the host bundle.
  • No particular build setup beyond an ESM import.

Payment security, PCI & CSP

Integrating third-party components such as ticketshops traditionally forces a trade-off between user experience, performance and IT security. The micro-frontend resolves that conflict with a hybrid paradigm: the interaction flow renders in the native light DOM of your page while the payment step stays strictly isolated — everything harmless happens in your page, the payment itself never does.

No payment data in the light DOM

The complete purchase preparation — event, tickets, add-ons, cart, registration — renders natively in your page. The moment the user starts a redirect payment, the sensitive flow leaves your page entirely: a normal top-level navigation hands over to the payment provider's dedicated, external payment page, and the return trip resumes the journey and completes the order. Card or bank data is entered only there — the micro-frontend never renders, transports or stores payment credentials in your page.

Minimal PCI scope

Because no credit-card or bank data is ever processed, transported or stored in your host application, your environment stays in the minimal PCI-DSS scope of the classic redirect model (typically SAQ A — the binding classification is always made by your acquirer or QSA). Payment methods without a redirect, such as invoice, complete server-side — with no payment data in the browser at all.

Lean Content Security Policy

On top of your existing policy, the micro-frontend itself needs only two allowances: connect-src for the ADITUS API origin (every shop call goes through it) and img-src for the ADITUS asset URLs if you show the event banner or article images. In particular, it needs no script-src or frame-src entries for payment providers, because the payment step is a plain top-level redirect: no payment scripts and no payment frames ever load inside your page.

The best of both worlds

Classically you had to choose: an iframe (isolated, but rigid and a visual foreign body) or a fully integrated API build (seamless, but your security team suddenly owns the full PCI scope). The micro-frontend resolves that dilemma: the harmless interaction — browsing, cart, ticket configuration — lives natively in your page; the critical interaction — collecting money — is cut off hard and isolated externally.

How it differs from the market alternatives

Versus the classic iframe

Iframes are considered secure thanks to strict browser isolation, but they behave like isolated documents: they inherit neither global styles such as typography or responsive layout rules, nor is their content reliably indexable for crawlers; for screen readers they often mean a media break. The micro-frontend renders the entire purchase preparation — article selection, cart, registration — as real HTML directly into the DOM of your page, making it as accessible and responsive as your own markup. The isolating effect of an iframe is recreated only at the moment of payment: the critical data stream is handed over completely, via a top-level redirect, to the external, dedicated payment page.

Flexibility versus pure Shadow DOM

Many web components encapsulate themselves in a shadow DOM to avoid CSS specificity conflicts — in practice this often leads to incomplete branding, because global styles are blocked and design rules have to be passed through laboriously via part attributes or custom properties. The micro-frontend uses the native CSS cascade instead: its base styles live in @layer aditus-shop, a deliberately low layer — every unlayered CSS rule of your page wins automatically. Your corporate design is inherited natively, without specificity wars against the shop's UI components.

Resource efficiency & bundle size

Monolithic widgets frequently ship their own HTTP clients, state management and UI libraries — and can force the browser to load redundant runtimes in parallel, hurting your page's load time (Core Web Vitals). The micro-frontend declares React and React-DOM exclusively as peerDependency and has zero runtime dependencies of its own: if your page already uses React 18 or newer, it hooks straight into the existing runtime. For non-React environments, the optional web component encapsulates the React dependency in its own isolated bundle, without occupying global variables on your page.

The approaches compared

CriterionClassic iframeShadow-DOM widgetADITUS micro-frontend
SEO & accessibilityContent lives in a separate document — often invisible to crawlers, harder for screen readers.Isolated; discoverability and accessibility typically need extra work.Native part of your DOM — indexable and accessible like your own markup.
Styling & brandingRigid: no CSS inheritance, customization only via postMessage APIs.Global styles do not reach inside; theming only via passed-through properties and parts.Your CSS wins by definition via @layer; the base skin is overridable or fully off.
Bundle & performanceTypically loads a complete second app including its runtime inside the frame.Often ships its own runtime and duplicate dependencies.Uses your page's React; zero runtime dependencies of its own.
Payment security & PCI scopeStrongly isolated — but at the price of UX, design and responsiveness.Payment data would flow through the host DOM if it were captured there.Selection in the light DOM, payment fully isolated on the external payment page.

How you obtain the micro-frontend

The micro-frontend is not on public npm and not on a CDN. It is delivered as a source package straight from your ADITUS instance: TypeScript, ESM, fully typed, with zero runtime dependencies of its own — your bundler (Vite, webpack, Next.js, Nuxt) compiles it together with your app like your own code. The import name in the snippets on this page (@workspace/aditus-shop-embed) is the package name from our reference integration; the downloadable package is named @aditus/shop-embed — the API is identical.

What the delivery contains

The complete adapter as readable TypeScript source with type declarations for every config option, callback and event. React and react-dom stay peerDependencies — your page provides them, so nothing collides in your bundle and there is no second React.

Getting the package

The package is identical for every host and downloads directly from this instance, no credentials required: {{API_BASE_URL}}/api/embed/v1/aditus-shop-embed.tgz. Install it straight from that URL (npm install accepts a tarball URL — use the link target on your instance, path /api/embed/v1/aditus-shop-embed.tgz) or unpack it into your repo. What the onboarding actually provides is your publicKey with its domain whitelist; without it the micro-frontend stays inactive. Contact: contact page. Hosts without their own build step (WordPress, plain HTML) do not need the source package at all: they use the self-hosted Web Component bundle described in the next section.

Web Component: no build step required

For pages without their own bundler — WordPress, Typo3, plain HTML — the micro-frontend also ships as a prebuilt, self-hosted bundle: one script tag registers the <aditus-shop> element, React included, no npm, no build. It is delivered from your ADITUS instance under a versioned URL (/api/embed/v1/…), not from a public CDN — for this demo system that is https://developers.aditus.com/api/embed/v1/aditus-shop.js. The URL is deliberately public and requires no credentials: the bundle is plain published code, the actual gate is your publishable key with its domain whitelist plus the session. This path is strictly optional and purely additive: hosts with their own build keep integrating the source package via mount() exactly as documented on this page — same journey, same CSS hooks, same 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"
  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>
AttributeMeaning
public-keyYour publishable key (domain whitelist applies). With only this attribute set, the element mints its own browser session — the right mode for pages without a backend.
session-tokenA session minted by YOUR backend (server-to-server, see below). If set, the element never mints itself — your page owns the session lifecycle.
api-baseBase URL of the ADITUS API. Defaults to the origin the bundle script was loaded from — usually you never set this.
cultureShop language/locale, e.g. de-DE or en-GB. Changing the attribute re-localizes in place — the user's journey is preserved.
article-layout / event-layout / article-select-mode / show-headerThe flat presentation options, same values as the mount() config (cards, quantity, …). The ENTRY POINT (event picker vs. a pinned event) is not an attribute: it is fixed server-side when the session is minted.
survey-columnsAnswer-option columns (1–4) for RadioButtonList/CheckBoxList survey questions — same as surveyColumns in the mount() config. Unset keeps the single-column default (or the client default stored server-side); narrow viewports collapse back to one column.
base-stylesSet to "false" to drop the neutral default skin and style every hook yourself.

Session & self-healing

The session rules of this page apply unchanged. With a backend, mint server-to-server and set session-token — the element never mints on its own then. Without a backend, set only public-key: the element mints a browser session via the domain-gated endpoint and transparently re-mints when it expires, so the user never dead-ends. Light DOM as always: no iframe, no shadow DOM — your CSS reaches every documented hook.

Events & versioning

The event bus surfaces as plain DOM CustomEvents on the element (bubbling, payload in event.detail) — cart:update, checkout:complete and the analytics mirror, same names and payloads as handle.on(). The URL carries the major version: v1 receives compatible updates in place; a breaking change ships as /embed/v2/ so nothing changes under your page unannounced. Everything beyond the flat attributes — theme objects, callbacks like onUserRequired, card content — remains a source-package feature by design.

Attributes, not properties

The element is configured exclusively through HTML attributes — and attributes are always strings. base-styles="false" is the literal string "false"; the element parses it, you never assign JavaScript properties. Frameworks bind hyphenated names like public-key as attributes anyway, so no special binding syntax is needed. Anything not expressible as a flat string — theme objects, callbacks, card content — is deliberately not an attribute: for that, use the source package.

Resetting the journey deliberately

The one exception to "attributes only": the element exposes a clearJourney() method — the build-less counterpart of the source package's clearJourney() export. Call document.querySelector("aditus-shop").clearJourney() (e.g. from a "start over" button) before re-adding or reloading the element, and the locally persisted journey record for the current page is dropped — the next mount starts fresh at the entry point instead of resuming. Only the local resume pointer is removed; any server-side cart state is untouched.

Updates without remounting

Changing an attribute on the live element updates the running instance in place — exactly like handle.update() in the source package. Presentation attributes (culture, layouts) preserve the user's journey; only changing the session identity (public-key, session-token, api-base) re-establishes the session. Removing the element from the DOM unmounts the shop cleanly, including all listeners.

SSR hosts: Next.js & Nuxt

The micro-frontend is deliberately client-only. Its content is live and session-bound — availability, prices and the user's cart exist only for an authenticated session at request time, so there is nothing meaningful a host server could pre-render. mount() creates a fresh client-side React root (createRoot); server-rendered markup inside the container is not hydrated. That makes SSR integration simple and predictable: render a placeholder on the server, mount on the client, and reserve the space so nothing jumps.

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 }} />;
}

Avoiding layout shift (CLS)

Give the mount container a min-height that roughly matches the first shop view, and optionally render your own skeleton inside it — server and client produce the same placeholder, so there is no hydration mismatch. The adapter renders immediately after mount and replaces the placeholder in one paint. Do not attempt hydrateRoot on the container: there is no server-rendered shop tree to attach to.

The session in an SSR host

SSR hosts have a natural advantage: the server runtime that renders the page can also mint the session — server-to-server with the secret, exactly like the mint flow below (Next.js: Route Handler or Server Component; Nuxt: server route). Hand the resulting opaque token to the client component as a prop or payload. The secret never appears in client code, and the token stays in memory.

Next.js specifics

Mark the mounting component "use client" and mount in useEffect (see snippet). With the Pages Router, next/dynamic with ssr: false achieves the same. Return the handle from the effect as cleanup so client-side route changes unmount cleanly. A culture switch on the running instance goes through handle.update() without resetting the journey; a new session token remounts (as in the snippet) — the cart survives that too, because the micro-frontend restores the journey on mount.

Nuxt / Vue specifics

Wrap the mount target in <ClientOnly> or mount in onMounted — the adapter needs a real DOM element in the browser. Remember the host-bundle requirement: react and react-dom 18+ must be installed as dependencies of your Nuxt app; the shop renders into its own container and does not interfere with Vue's virtual DOM.

The micro-frontend needs a session

No token

No session token — the micro-frontend stays inactive

Without a session token the micro-frontend renders a neutral notice and starts NO API calls at all. No cart can be filled, no registration, no checkout. A session is mandatory before the shop activates — but it does not have to be tied to a known user: a guest session is enough to browse and fill the cart, and the real user can be established later at checkout (see the optional-user flow below).

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.
});
Per-host user

Session token — a specific user

Your backend mints a short-lived token tied to one user. The micro-frontend then acts as that user: their cart, their data. The token is opaque, expiring, single-user and bound to your publicKey — and your secret never reaches the 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.

A page reload does not lose the journey. The micro-frontend remembers cart and step locally, re-fetches the live cart from the server on mount and continues exactly where the user left off — even though your page mints a fresh session token on every load. If the cart has expired server-side, the journey simply starts fresh. Your page does not have to do anything for this.

Want a deliberate fresh start instead — a real "start over" button? The package exports clearJourney(): call it before mounting (or re-mounting) and the locally persisted journey record for the current page is dropped, so the next mount starts fresh at the entry point. Only the local resume pointer is removed; any server-side cart state is untouched.

Optional: browse anonymously, sign in at checkout

You do not have to know who the user is up front. Mount with a guest session and the micro-frontend lets an anonymous visitor browse events, pick articles and fill the cart. Only when they move past the cart — toward registration and checkout — does it ask you to establish the real user, via the onUserRequired callback. The rest of the flow then continues with the cart preserved.

  1. 1

    Mount with a guest session

    A session token is still required to activate the shop — but it can be a guest session: simply mint it WITHOUT the email field, no placeholder address needed. The visitor browses and fills the cart under it.

  2. 2

    Leaving the cart triggers onUserRequired

    The moment the visitor advances past the cart, the micro-frontend calls your callback once and shows a neutral pending state. Authenticate the user (login/SSO), then mint a fresh, user-bound session the exact same way as the first one: server-to-server with your secret. The later user is established via that machine-to-machine mint too — the secret never reaches the browser and the user is never set in client code. Your callback only relays the resulting opaque token. Crucially, your backend derives that identity from its OWN authenticated session (cookie/JWT); it must never accept the email or user id from the browser, or a malicious visitor could request someone else's session and read their personal data via the prefill.

  3. 3

    Return the new token — the cart carries over

    Return the fresh session token and the micro-frontend swaps it in and binds the identified user to the existing cart — the cart, its articles and any timeslots survive unchanged. Then it enters registration. Return null to abort — the visitor simply stays on the cart, no error.

  4. 4

    Omit onUserRequired for a fixed user

    If you leave the callback out, nothing changes from the classic flow: mount with a user-bound session and the micro-frontend acts as that user from the very first step.

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 wie
// gehabt, nur OHNE das Feld email (keine Platzhalter-Adresse nötig). 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);

The host page authenticates the user

ADITUS does not authenticate your end users. Your site is the single source of truth for who the user is — you run your own login, SSO or session. ADITUS only trusts a session token that your backend mints with a secret key. The trust chain is: your auth proves identity, your secret-minted token vouches for it, the micro-frontend presents it.

  1. 1

    Your site authenticates the user

    Login, SSO, member session — entirely your own. ADITUS is not involved here.

  2. 2

    Your backend mints a session

    Server-to-server with the secret key. You pass who the micro-frontend should act as (email) and bind it to your publicKey.

  3. 3

    The page mounts the micro-frontend

    The opaque token is handed to the browser and passed as sessionToken to mount(). The secret stays on your server.

  4. 4

    Every call carries the session

    The micro-frontend sends X-Aditus-Session on each request; the proxy resolves the cart's user from the session.

  5. 5

    No token → micro-frontend stays inactive

    Without a session the micro-frontend never activates: it shows a neutral notice and makes no calls — no cart, no checkout. Mint a session to switch it on.

1. Mint on your backend

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: {
    // Mint-Secret DEINES Clients (im ADITUS-Admin erzeugt), nur server-seitig.
    Authorization: `Bearer ${process.env.ADITUS_MINT_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 (weglassen = anonyme Gast-Session)
    externalUserId: user.id,           // optionale Metadaten (deine User-ID)
    ttlSeconds: 1200,                  // optional: Lebensdauer (Default 20 min, max 24 h)
    eventSlug: "ExperienceDaysv52024", // optional: Einstieg direkt in dieses Event (sonst Eventübersicht)
  }),
});

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

2. Mount with the token

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. Revoke on logout

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_MINT_SECRET}` },
});
// { revoked: true }

The finished order returns to your page

When a ticket purchase completes, you do not need a separate order lookup. Because the micro-frontend runs natively in your page (light DOM, not an iframe), it hands the finished order straight back to your code via the onComplete callback — a direct JS call, no postMessage. The micro-frontend has already resolved the order for you, so you receive the order number and the ticket links, not just an 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);
  },
});

What you receive

onComplete fires once per successful checkout with the resolved order: number (the order id / order number — your identifier), status, buyer (name & email when present) and tickets — each ticket carries typed links with kind “pdf”, “apple” or “google”. onError fires on any unrecoverable error in the flow.

Redirect payment methods

Methods that stay on the page (invoice, prepayment, inline card) fire onComplete right away. A method that sends the browser to an external payment page (e.g. Saferpay) is handled by the Micro-Frontend end to end: it derives the return URLs from the page it runs on, sends the user out, and on the way back it finalizes the order and fires onComplete — a full round-trip with zero code on your side. Only if the return must land on a DIFFERENT page do you override redirectUrls.successUrl / cancelUrl / errorUrl.

GA4 / e-commerce events (optional)

The micro-frontend can report the shopping funnel to your analytics — but only if you ask it to. It bundles no gtag, no GTM and no GA SDK, loads nothing, sets no cookies and sends nothing on its own. You add one optional callback, onEvent, and receive typed events already shaped in Google's GA4 Enhanced-Ecommerce schema. Omit onEvent and nothing changes. Which tool you feed, and whether you have consent to send, stays entirely on your side.

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);
  },
});

Already in the GA4 shape

Each event carries a GA4 event name plus its standard params: items (with item_id, item_name, price, quantity, item_category, and — where it applies — discount and coupon), value and currency; add_payment_info adds payment_type and purchase adds transaction_id. Destructure name and pass the rest straight to gtag("event", name, params) — no re-mapping needed.

Consent stays yours

onEvent is just a normal JavaScript function in your page that we call directly. The Micro-Frontend itself sends no analytics anywhere — nothing leaves the page until your handler sends it, so your consent management stays in full control: gate the forward on consent, route to GTM's dataLayer instead of gtag, batch, or drop events entirely. Any error your handler throws is caught and isolated, so it never breaks the user flow — keep the handler light (a gtag/dataLayer push) since it runs inline.

EventFires when
view_item_listThe user sees the article list (fires once per shown set).
add_to_cartAn article or add-on is added to the cart.
remove_from_cartA cart line or add-on is removed.
view_cartThe cart view is shown (once per cart).
begin_checkoutThe user leaves the cart towards registration / checkout.
add_payment_infoA payment method is chosen (carries payment_type).
purchaseThe order is placed (carries transaction_id, value, items).

Event bus: handle.on / handle.off (optional)

Besides the config callbacks, the handle returned by mount() carries a small, fully optional event bus. Subscribe with handle.on(event, listener) — it returns the matching unsubscribe function — and detach with handle.off(event, listener). If you never call on(), nothing changes: the bus adds no dependencies, sends nothing and costs nothing. It is pure observation for your page — a mini-cart badge, a confirmation banner, your own logging — and it complements the callbacks rather than replacing them.

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();
EventFires when
cart:updateThe cart changes: created, item added or removed, or emptied. Carries cartId, itemCount, value, currency and the lines as GA4-shaped items.
checkout:completeThe order is placed — same moment and same resolved order object as config.onComplete.
analyticsMirror of the GA4 funnel above: every onEvent payload is also emitted on the bus, unchanged.

Listeners survive update()

Subscriptions live on the handle, not on a render: handle.update({ ... }) re-renders the Micro-Frontend but keeps every listener attached. Unmounting via handle() detaches all listeners automatically — you only need off() (or the returned unsubscribe) when you want to stop listening while the shop keeps running.

Isolated, never blocking

A listener that throws is caught and isolated — it never breaks the user flow and never affects other listeners. The “analytics” event mirrors the GA4 funnel with payloads identical to config.onEvent, so you can consume the funnel via the bus, the callback, or both; the same consent rule applies: nothing leaves the page until your code sends it.

publicKey vs. sessionToken

Keep the two separate. They answer different questions and are paired, not interchangeable.

publicKey — origin trust

Publishable. It ships in your client code, so it is NOT a secret and grants nothing on its own. The backend enforces a per-key domain whitelist: a key only works from its registered domains. It answers “which site is this?”

sessionToken — identity

Secret-minted on your backend, opaque and short-lived. It answers “who is the user?” A session is bound to the publicKey it was minted for: a session request must also send that key, and a mismatch is rejected (403).

Security rules

  • The secret key lives only on your server — never in client code, bundles or env files shipped to the browser.
  • Keep the token in memory only. Do not persist it to localStorage; refresh it by re-minting.
  • Tokens are short-lived (default 20 minutes, max 24 hours) and single-user.
  • Revoke the session on logout so a leaked token cannot be replayed.
  • Always pair the session with its publicKey — a missing or mismatched key is rejected (403).
  • A bad or expired session is a hard 401 — never a silent fall-back to the demo user.

Endpoint & response reference

POST/api/shop/sessionmint — Bearer secret
DELETE/api/shop/session/:tokenrevoke — Bearer secret

In this demo, minting runs through the proxy at the paths above. In production the session is minted in the ADITUS core — the contract is identical. Both calls are server-to-server: your backend authenticates with your client's mint secret (generated in the ADITUS admin console) as a Bearer token. One mint per login is the normal pattern; the resulting token is all the browser ever sees.

You can try this exact flow with your own credentials in the integration tester — mint, mount, walk the journey.

Mint a session — request

HeaderValueMeaning
AuthorizationBearer <mint-secret>Your client's mint secret, generated in the ADITUS admin console. Server-side only — it must never be shipped to a browser or app bundle.
Content-Typeapplication/jsonThe body is a JSON object.
Body fieldTypeMeaning
publicKeystring · requiredThe publishable key the session is minted FOR. It must be the same key the micro-frontend mounts with — every later shop call is checked against it (403 on mismatch). The Bearer secret must belong to this key's client.
emailstring · optionalThe shop user the session acts as. Take it from YOUR authenticated server session (login/SSO cookie or JWT) — never from the browser request, or a visitor could obtain someone else's session. OMIT it to mint an ANONYMOUS session: the visitor can browse and fill the cart, but the micro-frontend blocks the step past the cart until your onUserRequired callback supplies a user-bound session (see the optional-user flow). A present but malformed email is still rejected (400).
externalUserIdstring · optionalYour own user id, stored with the session as metadata (useful for support and log correlation). Not interpreted by ADITUS.
ttlSecondsnumber · optionalSession lifetime in seconds. Default 20 minutes, capped at 24 hours. When it expires, shop calls return 401 session_expired — mint a fresh token then (e.g. via the onSessionExpired hook).
eventSlugstring · optionalFixes the journey's ENTRY POINT server-side: set, the micro-frontend starts directly on this event's article selection; omitted, it starts on the event overview. Because it is part of the minted session, the browser cannot manipulate it. Invalid characters yield 400 invalid_event_slug; a slug that resolves to no live event falls back to the event overview.

Mint a session — response (200)

FieldTypeMeaning
sessionTokenstringOpaque token (sess_…). Hand it to the browser and pass it to mount() as sessionToken; the micro-frontend sends it as X-Aditus-Session on every shop call. It contains no user data and cannot be decoded.
expiresAtnumberExpiry as a Unix timestamp in milliseconds. Purely informational for your own scheduling — the micro-frontend reacts to the 401 on its own.

Revoke a session

DELETE /api/shop/session/:token with the same Bearer secret — the secret must belong to the client the session was minted for. Call it on logout so the token dies with your own session. Response: { "revoked": true } (or false if the session had already expired). Revoking is idempotent and safe to fire-and-forget.

Errors are explicit

StatusCodeMeaning
503session_not_configuredminting is disabled for this client: no mint secret has been generated in the admin console yet.
401unauthorizedthe mint secret is wrong or missing. Check that the secret belongs to the client of exactly this public key (rotated secrets invalidate the old one immediately).
400invalid_public_keythe public key is malformed or not registered. It must look like pk_… and belong to a configured client.
400invalid_emailthe shop user email is missing or not a valid address.
400invalid_tokenthe session token in the revoke call is malformed. Pass exactly the sessionToken returned by the mint call.
429rate_limitedtoo many mint/revoke calls from your IP. Wait a minute and try again.
403public_key_invalidthe X-Aditus-Public-Key header is malformed. It must look like pk_… exactly as issued during onboarding.
403public_key_unknownthe X-Aditus-Public-Key is well-formed but not registered (or deactivated). Check for typos and that the key's client is active.
403public_key_origin_unresolvedthe request carried a publicKey but no usable Origin/Referer header, so the domain whitelist cannot be checked. Browsers send Origin automatically; server-side calls must not use the publicKey header.
403public_key_domain_not_allowedthe request's origin domain is not on this publicKey's whitelist. Add the domain during onboarding (or in the admin console) before going live on it.
401invalid_sessionthe shop call carried a malformed X-Aditus-Session token — never a silent fall-back to the demo user. Pass exactly the sessionToken (sess_…) returned by the mint call.
401session_expiredthe X-Aditus-Session token is unknown, revoked or expired — never a silent fall-back to the demo user. Mint a fresh session server-to-server.
403session_requires_public_keya session was sent without its X-Aditus-Public-Key header (origin pinning is mandatory once a session is in play).
403session_key_mismatchthe X-Aditus-Public-Key does not match the key the session was minted for.
403anonymous_sessionthe session was minted WITHOUT an email (anonymous) and only allows browsing and the cart. Registration, payment and checkout require a user-bound session — mint one via onUserRequired.

Now make it yours

Identity is set — next, brand the micro-frontend. The styling tool generates a ready-to-paste theme config and explains every parameter, with a live preview.

Open the styling tool