npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ticketlayer/backstage-elements

v0.2.2

Published

Ticketlayer Backstage Elements - embeddable organiser-facing widgets for partner admin surfaces, built with Stencil

Readme

@ticketlayer/backstage-elements

Backstage Elements are the organiser-facing component layer: the orders list with refund actions, an event's tickets and ticket types, the station's device selector, the seat map designer. They are the admin-side counterpart of @ticketlayer/elements (the buyer-facing components).

They serve two kinds of host, from one source:

  • Partner admin surfaces - Rowbot Manage, the Webflow Designer Extension - where a partner's own users manage ticketing without ever logging in to Backstage. These authenticate with an embed session minted by the partner's server with its organisation API key (POST /embed/sessions, see @ticketlayer/backstage).
  • Ticketlayer's own staff apps - Manage and the box office, in this monorepo - which authenticate with the staff session they already hold and must not have to mint an embed session for themselves.

That second audience is the design, not an afterthought: building our own apps out of these elements is what makes them a first-class thing other people can build with. See Credentials for the one interface that serves both.

Built with Stencil, published as Web Components (tlb-*) and React wrappers.

On npm since 0.2.0. It lives in the ticketlayer/backstage-app monorepo at packages/elements, and releases from the root .github/workflows/release.yml through changesets.

How this relates to the embed widgets

Today the organiser widgets exist as pages in Manage (src/app/embed/*: events, event, orders, order) that a partner mounts in an iframe through the @ticketlayer/backstage/embed loader. The iframe is the transitional mechanism:

  • tlb-orders today is a thin wrapper: it takes a sessionProvider, mounts the orders widget through createBackstageEmbed, and relays the iframe contract (tl:navigate, tl:event, tl:ready, tl:session-expired, tl:error; protocol version 1) as DOM events (tlbOrderSelected, tlbRefundRequested, tlbReady, tlbSessionExpired, tlbError, tlbEvent).
  • Once the SDK exposes the underlying operations (orders list/get, refund, ticket types, layouts) behind embed-session credentials, each element renders natively in the host page behind the same props and events. Hosts written against tlb-orders today do not change when that happens.

The loader lives in ticketlayer/backstage at sdk/typescript/src/embed; the widget side of the contract in this monorepo's apps/manage/src/lib/embed-contract.ts.

Elements

| Tag | Status | Credentials | What it is | |-----|--------|-------------|------------| | tlb-orders | available | embed | The account's orders list, order detail and its actions (refund, resend confirmation), mounted from the embed orders widget | | tlb-device-selector | available | staff, embed | The station's location, receipt/ticket printer and card terminal, and the terminal charge. Renders natively | | tlb-event-tickets | planned | embed | An event's ticket types and issued tickets (renders a "coming soon" panel) | | tlb-seat-designer | planned | embed | The seat map designer for an event layout (renders a "coming soon" panel) |

tlb-orders

<script type="module" src="https://cdn.ticketlayer.com/backstage-elements/v1/ticketlayer-backstage-elements.esm.js"></script>

<tlb-orders id="orders" base-url="https://backstage.ticketlayer.com" account-id="acc_123"></tlb-orders>
<script>
  const orders = document.getElementById('orders');
  // Your server mints the embed session with the organisation API key.
  orders.sessionProvider = async ({ accountId, reason }) =>
    (await fetch(`/api/ticketing/session?account=${accountId}`).then((r) => r.json())).token;
  orders.addEventListener('tlbOrderSelected', (e) => console.log(e.detail.orderId));
  orders.addEventListener('tlbRefundRequested', (e) => console.log(e.detail));
</script>

React:

import { TlbOrders } from '@ticketlayer/backstage-elements/react';

<TlbOrders
  baseUrl="https://backstage.ticketlayer.com"
  accountId={club.ticketlayerAccountId}
  sessionProvider={({ accountId }) => mintSession(accountId)}
  onTlbOrderSelected={(e) => router.push(`/manage/orders/${e.detail.orderId}`)}
  onTlbRefundRequested={(e) => toast(`Refund requested for ${e.detail.orderId}`)}
/>

Props: baseUrl (the Backstage app origin), sessionProvider (({ accountId, reason: 'mount' | 'expired' }) => Promise<string> | string; called on mount and again when the widget reports its session expired), accountId (passed to the provider and echoed on every event), filters ({ status, search, from, to } or a JSON string; reserved - appended to the widget URL as filter.* parameters that the widget ignores until Manage reads them; the native render applies them directly), initialHeight, loader (replace createBackstageEmbed; used by tests). Methods: showList(), openOrder(id), setSession(token).

The embed session needs the orders.read scope; refunds need payments.refund once that scope exists (G3 in the platform plan). The Backstage deployment must list the host origin in EMBED_FRAME_ANCESTORS.

tlb-device-selector

The station's active location, receipt/ticket printer and card terminal, and the card charge that goes with them.

It never touches local hardware. Everything is the devices API: GET /locations, GET /devices/printers, and GET /devices whose relay devices carry the payment_terminal peripherals the terminal list is flattened from. chargeOnTerminal queues a payment.charge command on the relay device and polls GET /devices/commands/{id} until the relay reports a result - the stagehand relay drives the terminal and posts the result back, so there is no 127.0.0.1, no Local Control Token in the browser and no LAN reachability.

React:

import { TlbDeviceSelector } from '@ticketlayer/backstage-elements/react';
import { chargeOnTerminal } from '@ticketlayer/backstage-elements';

<TlbDeviceSelector
  credential={credential}
  onTlbDeviceSelectionChange={(e) => setTerminal(e.detail.terminal)}
/>;

const result = await chargeOnTerminal(credential, {
  terminal,
  amount: 1250,
  currency: 'GBP',
  reference: order.id,
  onStage: (stage) => setStage(stage),
});

Props: credential (the first-party path), sessionProvider + apiBaseUrl + accountId (the partner path), label. Events: tlbDeviceSelectionChange, tlbReady, tlbError. Methods: refresh(), getSelection(), select({ locationId, printerId, terminalId }), chargeOnTerminal(request).

Reading the selection elsewhere in the page. The selector drives a small store keyed by the credential's tenant, and a host subscribes to it - that is how a payment panel somewhere else knows which terminal the header selected, without the host having to be React:

import { deviceStoreFor } from '@ticketlayer/backstage-elements';

const store = deviceStoreFor(credential);
const unsubscribe = store.subscribe((state) => render(state.terminal));
await store.load(credential); // idempotent: concurrent loads share one call

The selection persists in localStorage under backstage-device-<scopeKey>-location|printer|terminal, so it never leaks across organisations.

The devices routes are x-tl-surface: staff and are reachable with a staff session today. See Known limitation below for the embed path.

Known limitation: the embed path cannot reach the devices API yet

The element supports both credentials, but the API (ticketlayer/backstage) does not yet let an embed session through to /v1/devices/*:

  • SCOPE_CATALOGUE in src/shared/auth/scope-policy.ts in ticketlayer/backstage has no devices.read, and an embed session's scopes must be a subset of its API key's, which are catalogue scopes. So the read permissions (devices.list, locations.list) cannot be granted to a delegated token.
  • devices.dispatch classifies as neither read nor write, and permissionAllowedByScopes denies everything else for delegated tokens, so the terminal charge would stay denied even with a read scope.

Adding devices.read (and a dispatch decision) to the scope catalogue is the the API change that opens it. Until then the partner path is exercised in tests against a fake API, not against a live one, and a partner surface should use tlb-orders and the other embed elements.

Credentials

Every element consumes one credential interface and the host picks the implementation. The element never holds a token, never mints one, and has no organisation or account attribute: the tenant comes from the credential's own signed claims. That is the platform's rule - scope comes from the credential, never from a prop - and it is what stops a prompt, a query string or a partner page widening what an element can see.

interface TlbCredential<TApi> {
  readonly kind: 'embed' | 'staff';
  /** The credential's tenant, from its token claims. Namespaces per-station
      preferences; never sent to the API. */
  readonly scopeKey: string;
  /** The authed API surface for one batch of calls, resolved per use so a
      token can rotate underneath. */
  resolve(): Promise<TApi>;
}

TApi is the slice of the Backstage SDK client an element calls, named one operation at a time (TlbDeviceApi for the device elements). A real BackstageClient satisfies it structurally, and so does a fake in a test.

The partner path: an embed session

Unchanged. Set sessionProvider (and apiBaseUrl, the the API origin) and the element builds the credential itself:

<tlb-device-selector id="devices" api-base-url="https://api.ticketlayer.com" account-id="acc_123"></tlb-device-selector>
<script>
  document.getElementById('devices').sessionProvider = async ({ accountId }) =>
    (await fetch(`/api/ticketing/session?account=${accountId}`).then((r) => r.json())).token;
</script>

The token is checked to be an embed session (token_use: 'embed', audience backstage-embed) before it is used, so the partner path cannot become a way to present a staff token. the API would reject one anyway; failing in the element means it never leaves the page.

The first-party path: the staff session the app already holds

A Backstage app hands the element the authed SDK client it is already using. No embed session is minted for a staff user, and no token is copied anywhere.

// Components from /react; the framework-free helpers from the package root.
import { TlbDeviceSelector } from '@ticketlayer/backstage-elements/react';
import { createStaffCredential } from '@ticketlayer/backstage-elements';

const credential = createStaffCredential(client, {
  // The tenant is read from this token's signed org_slug claim. scopeKey is
  // the documented fallback for cookie-mode hosts, whose token the client
  // reads itself; it must come from the signed-in session.
  accessToken: () => token,
  scopeKey: orgSlug,
});

<TlbDeviceSelector credential={credential} />

createStaffCredential refuses an embed session token, the mirror of the embed path refusing a staff one, so neither path is a route into the other.

Where elements-core fits

TKT-68 is building packages/elements-core in the live monorepo: the shared core shape, manifest type and catalogue emitter. Nothing here invents that abstraction. When it lands, TlbCredential is the seam: it is the one thing every element takes from its host, so it is what elements-core should own (with the embed and staff factories as the two reference implementations), and src/credentials/ moves wholesale. The device store and the typed devices calls stay here - they are this element family's domain, not core.

Theming

Every element reads --tlb-* custom properties and falls back to the buyer-facing --tl-* set, then to a built-in default:

color: var(--tlb-foreground, var(--tl-foreground, #111827));

So a page that already carries a channel theme (injected by @ticketlayer/live) gets matching admin widgets for free, and a host that wants the admin widgets in its own palette (Rowbot Manage deliberately does not wear club colours) sets --tlb-* on a container. No --tlb-* default is defined globally, on purpose: it would shadow the --tl-* fallback.

The catalogue

dist/elements.catalogue.json (built by pnpm run build, shipped in the package) describes every element for docs, the React wrappers and an llms.txt: one entry per tag with

  • tag, name, category, description
  • status: available / beta / planned / deprecated
  • renders: embed-iframe today, native later, placeholder for planned
  • runtimes: web-component, react
  • sdkMethods: the SDK operations the element uses (or will use)
  • scopes the embed session must carry, and optionalScopes for actions
  • props, events, methods, cssProperties from the source (Stencil's docs-json)

The hand-maintained half is src/catalogue.manifest.json (same shape as the @ticketlayer/elements catalogue); scripts/build-catalogue.mjs merges it with docs/components.json and fails the build when a component has no manifest entry or vice versa. src/catalogue.spec.ts checks the same in pnpm test.

Development

What ships, and the ./react subpath

package.json declares exports for ., ./react and ./loader, and every one of them resolves to built output. Nothing published is TypeScript source.

  • The React wrapper sources live in react/src: the hand-written index.ts, plus components.ts and react-component-lib/ which the Stencil React output target generates (both gitignored).
  • pnpm run build compiles them with react/tsconfig.build.json to dist/react, which is what ./react points at. react/tsconfig.json stays noEmit and is what pnpm run typecheck uses.
  • Source and output sit at the same depth below the package root on purpose, so the relative specifiers in the sources (../../loader, ../../dist/types/index) resolve to the same files either way. Emitting anywhere else would mean rewriting every specifier after tsc.
  • scripts/patch-react.mjs rewrites the generated react/src/components.ts to import ../../dist/types/index and ../../loader relatively. Stencil emits those imports by the root package name, which nothing can resolve from inside the package (the nearest package.json to that file is react/package.json, so Node's self-reference does not apply).
  • react/package.json is a private, unpublished manifest for a future standalone @ticketlayer/backstage-elements-react. It is not in the files array and does not ship.

Because ./react is compiled JavaScript, a consumer no longer needs this package in Next's transpilePackages.

Working on it

From the repository root (pnpm workspace, Turborepo):

pnpm install
pnpm --filter @ticketlayer/backstage-elements build       # stencil -> dist/, loader/, react/src/components.ts, docs; the catalogue; tsc -> dist/react
pnpm --filter @ticketlayer/backstage-elements test        # stencil test --spec (jest)
pnpm --filter @ticketlayer/backstage-elements typecheck   # the React wrappers (after a build)
pnpm --filter @ticketlayer/backstage-elements dev         # dev server with src/index.html
pnpm changeset                                            # describe a change for the changelog

An app in this monorepo consumes it as "@ticketlayer/backstage-elements": "workspace:*"; turbo builds it before the app, and the app resolves its dist/ and loader/ like any installed package.

@ticketlayer/backstage is a peer dependency (>=0.3.0 <5.0.0), and a devDependency at the version the apps here use so that pnpm installs one copy for both. It is a peer rather than a dependency because this library holds credentials: two copies of the SDK in one page means two token stores and two refresh cycles, and the symptom is intermittent auth failure. Every host already depends on it directly.

Mirrors @ticketlayer/elements (ticketlayer/live, packages/elements): same Stencil, React output target, jest and TypeScript versions, shadow DOM, per-component readme.md from docs-readme, React wrapper sources in react/.

Roadmap

  1. Transitional (now): tlb-orders over the embed orders widget; tlb-device-selector rendering natively under either credential; placeholders for tlb-event-tickets and tlb-seat-designer.
  2. Widgets as elements: tlb-events, tlb-event (details, tickets, occurrences) and tlb-order wrapping the remaining embed widgets, so a partner page composes the whole admin surface from elements.
  3. Native renders: as the SDK gains embed-session operations (orders list/get, refund, ticket types, occurrences, layouts), each element drops its iframe and renders in the host page with the same API. The seat map designer is the largest of these and stays an iframe longest.
  4. CDN build alongside the live Elements bundle: still to come.