@spotlight-events/storefront-js
v0.1.3
Published
Headless storefront SDK for the Spotlight embedded platform: typed availability reads and a subscribable checkout state machine over the publishable-key storefront API.
Readme
@spotlight-events/storefront-js
Headless storefront SDK for the Spotlight embedded platform. Typed availability reads and a subscribable checkout state machine over the publishable-key storefront API — the protocol's sharp edges (session-token custody, the free/paid fork, Stripe.js on the connected account, webhook-driven fulfillment, the five-minute session lifecycle) handled structurally instead of by every integration re-learning them.
- Zero runtime dependencies.
@stripe/stripe-jsis an optional peer, loaded lazily only when a paid checkout proceeds. - ESM + CJS, Node 18+, framework-free. React binding is one line (below).
- Wire contract shipped in the package (
contract/wire-contract.json) and asserted on both sides: the backend's CI runs a spec against the live service, this package's tests assert the classifier, types, and recorded fixtures against the same file.
Install
npm install @spotlight-events/storefront-js
# paid checkout also needs the optional peer:
npm install @stripe/stripe-jsQuickstart
Everything is a subscribable store, and the flow methods are legal only in the phases they belong to — so the natural shape is: render every snapshot, act on the phases that ask for input.
import { createStorefront } from '@spotlight-events/storefront-js';
const client = createStorefront({
publishableKey: 'spt_pk_...', // dashboard: Integrations -> Storefront
apiBase: 'https://spotlight.events/api', // the default
});
// What can a visitor do with this event right now?
const desk = client.desk(eventId);
desk.subscribe((snapshot) => {
// { status: 'ready', mode: 'checkout', availability, actions, ... }
// Render the ticket list; enable your buy button once
// snapshot.status === 'ready' && snapshot.actions.checkout.
});
// From your buy button (desk is ready — checkout() throws otherwise).
// Creating a session reserves inventory for 5 minutes:
const flow = desk.checkout({
line_items: [{ ticket_type_id, quantity: 1 }],
buyer_email: '[email protected]',
});
flow.subscribe((s) => {
render(s.phase);
switch (s.phase) {
case 'reserved':
// Advance: free carts complete here; paid carts continue below.
void flow.proceed();
break;
case 'ready_for_payment':
// Paid only. Mount a card element from the flow's own Stripe
// instance (non-null in this phase), then confirm from your pay
// button:
if (flow.stripe) {
const card = flow.stripe.elements().create('card');
card.mount('#card-element');
payButton.onclick = () => void flow.confirmCardPayment({ card });
}
break;
case 'completed': // done — s.result
case 'charged_pending_confirmation': // charged; never re-offer payment
case 'payment_failed': // recoverable; show s.error
default:
break;
}
});
// After confirmation the flow polls until the fulfillment webhook lands
// -> phase 'completed'.React
Every desk and flow is a store with a stable getSnapshot and an immediately-firing subscribe — both safe to pass unbound:
const snapshot = useSyncExternalStore(flow.subscribe, flow.getSnapshot);Refresh survival
flow.serialize() returns a secret-bearing payload (sessionStorage is a good home); client.resumeCheckout(payload) rehydrates it. A resumed paid flow re-derives its PaymentIntent idempotently and asks Stripe for the intent status first, so an already-charged buyer is never re-shown a payable form.
Errors
Every failure is a SpotlightError: a coarse code derived purely from the HTTP status and endpoint class (never message matching), an optional fine reason, the display-safe server prose in displayMessage, and a retryable hint that is never true for mutations. reason prefers the server's machine-readable envelope code (so interpolated errors like sold-out resolve too, and the 409/410 session-expiry race collapses to session_expired); against older servers it falls back to an exact-match table over the backend's pinned copy, normalized to the same values the wire codes give — a given failure yields one stable reason regardless of server version (drifted copy degrades to undefined, never mis-classifies).
Honest limits
- There is no client confirm endpoint; fulfillment is webhook-authoritative. On paid orders the SDK can only observe completion by polling, and charged-pending is a real outcome the partner UI must handle.
- Tickets are never retrievable through this API. Spotlight emails them to
buyer_email, valid and ready to use, with a QR code and an Apple Wallet link per ticket. On the free pathresult.ticketsholds type-level summaries only (statuscompleted), and a sequential retry cannot recover them.result.claimableis alwaysfalse: tickets never need claiming. - The whole checkout must finish within 5 minutes of session creation; creating a session holds real inventory and is not retry-safe.
- A persistent 404 can mean the platform's storefront API is disabled, not that your event id is wrong; the two are indistinguishable by design.
- An expired result shortly after a successful charge can later become completed; never re-collect payment after a timeout.
- Event and organization metadata (
/public/*) is not part of this SDK's browser surface; fetch it server-side. - Origin-allowlisted keys fail closed on requests without an Origin header; server-to-server use needs a key without an origin allowlist.
- Rate-limit headers are not readable from browser code; the documented throttle table is the contract.
Versioning
SDK 0.x tracks Spotlight API v1 at contract version 0.1.3. Breaking changes ship only in minor bumps while 0.x; additive backend evolution (new fields, new enum values) never forces a release — every wire enum is an open union. 1.0.0 is gated on the Storefront Checkout API itself shedding its feature flag.
Development
pnpm install
pnpm test # vitest: contract, machine, http, errors, negative fixtures
pnpm typecheck # includes the compile-time brand/open-union assertions
pnpm build # esbuild (ESM + CJS, code-split paid module) + declarations
pnpm check:exports # attw packaging tripwire
pnpm check:size # bundle budgets: core <= 8 kB gz, paid chunk <= +3 kB
pnpm test:smoke # live free-path smoke vs the seeded dev storefront (env-gated)Releases are managed with changesets: pnpm changeset to record a change, pnpm changeset version to cut the version.
