Skip to content

Component Architecture Concepts

This page explains the architecture behind Seva’s web components, so you can make informed decisions when integrating them into your site.

Seva components are web components — browser-native custom HTML elements that work in any framework or plain HTML. They’re built with Lit, a lightweight library for building web components.

Each component:

  • Registers a custom element tag (e.g., <seva-auth>)
  • Uses Shadow DOM for style encapsulation — component styles don’t leak into your page, and your page styles don’t break the components
  • Accepts configuration via HTML attributes (e.g., tenant-slug, api-url)
  • Communicates outward via custom DOM events (e.g., seva:authenticated)

Each component is shipped as a standalone ES module:

seva-auth.js
seva-event-register.js
seva-cart.js

Load them with <script type="module">. Module scripts are deferred by default, meaning they don’t block page rendering. Shared code (Lit runtime, API client, etc.) is automatically split into a shared chunk (seva-shared-[hash].js) by the build system.

Components are configured through HTML attributes:

<seva-event-register
tenant-slug="my-club"
event-slug="annual-gala"
api-url="https://api.seva.tools">
</seva-event-register>

When an attribute changes, the component reacts automatically. For example, changing event-slug triggers a new API fetch for the updated event.

Some properties (like theme) are set via JavaScript rather than HTML attributes:

document.querySelector('seva-auth').theme = 'dark';

Components communicate through standard DOM CustomEvents with bubbles: true and composed: true (meaning they cross Shadow DOM boundaries). All Seva events use the seva: prefix.

The communication pattern is unidirectional:

  1. <seva-auth> fires seva:authenticated → your glue script sets auth-token on other components
  2. <seva-event-register> fires seva:add-to-cart<seva-cart> picks it up (automatically, via document-level listener)
  3. <seva-cart> fires seva:checkout-complete<seva-event-register> refreshes to show the registered state

This event-based architecture means components are loosely coupled. You can use <seva-auth> without <seva-cart>, or use <seva-cart> with your own custom registration form, as long as you dispatch the correct events.

Authentication tokens flow through HTML attributes, not a shared global store:

User signs in
→ seva-auth stores token in localStorage
→ seva-auth fires seva:authenticated
→ Your script reads auth.getToken()
→ Your script sets auth-token="..." on other components
→ Components use the token for API calls

This explicit wiring means you control exactly which components receive the token and when. It also means the auth component can be used on a different page from the other components — the token is persisted in localStorage and restored on page load.

Two things are persisted in localStorage:

Key patternDataComponent
seva-auth-{tenantSlug}User object and session token<seva-auth>, <seva-event-register>, <seva-cart>
seva-cart-{tenantSlug}Cart items (attendees, tickets, prices)<seva-cart>

Both are scoped to the tenant-slug value. Multiple tenants on the same domain won’t collide.

Each component renders inside a Shadow DOM root. This means:

  • Your CSS won’t affect component internals. You can’t style the sign-in form’s button by targeting button in your stylesheet.
  • Component CSS won’t affect your page. The component’s styles are fully encapsulated.
  • Layout works normally. Components participate in your page’s normal flow — you control their width, margin, and position from outside.

The theme property ('light' or 'dark') controls the component’s internal color scheme. Components use CSS custom properties internally for theming.

Each component creates its own ApiClient instance (configured by tenant-slug and api-url). The API client:

  • Adds the tenant slug to all requests
  • Attaches the auth token (if available) as a Bearer token
  • Handles error responses and surfaces them as component errors

The <seva-cart> component dynamically imports @stripe/stripe-js only when a paid checkout is needed. Stripe is never loaded on pages where all registrations are free. The Stripe publishable key comes from the API during checkout, so you don’t need to configure it on the frontend.