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

@bowlos/reservations

v0.24.0

Published

BowlOS Reservations SDK — headless client + React hooks for the Module 14 public API gateway. White-label booking, account, and flash-sale reads for third-party-hosted center sites.

Readme

@bowlos/reservations

Headless client + React hooks for the BowlOS public API gateway — white-label booking, customer accounts, and flash-sale reads for third-party-hosted center websites. Built for React/Next sites (including AI-agent-built ones) that book on a BowlOS operator's behalf, with Stripe Connect payments landing on the operator's own account.

  • Headless-first. A typed client + hooks; you own the UI. The booking engine is meant to match your site's design, not look like a themed plugin — so the primary path is the hooks. HEADLESS.md maps every built-in booking behavior to its hook + pattern (and how to adopt each SDK release). The drop-in components below are a reference, not the product.
  • Two entry points. @bowlos/reservations (React-free core) and @bowlos/reservations/react (provider + hooks). React is an optional peer.
  • Dual ESM + CJS, fully typed.

Quickest start: scaffold a site

npx @bowlos/reservations init my-center-site

Scaffolds a complete, runnable Next.js center site — /book, /account, and /flash/[slug] already wired, with the Stripe Connect payment handoff and a themeable provider. It ships both build styles as a working reference: /book is built from the headless hooks; /account and /flash use the themeable components. Keep what fits, swap the rest. Then:

cd my-center-site && npm install
cp env.example .env.local        # fill in your key + brand/center/product ids
npm run dev

Prefer to wire it by hand, or add the SDK to an existing app? Read on.

Install

npm install @bowlos/reservations

React is a peer dependency (only needed for the /react entry):

npm install react

Configure

You need a brand API key (minted by the operator) and the gateway base URL. The key is publishable — it's gated by a per-key origin allow-list, so it's safe in browser code (like a Stripe publishable key). Add your site's origin to the key's allow-list.

import { createBowlosClient, localStorageTokenStore } from "@bowlos/reservations";

export const bowlos = createBowlosClient({
  apiKey: process.env.NEXT_PUBLIC_BOWLOS_API_KEY!,
  baseUrl: "https://<deployment>.convex.site/api/public/v1",
  tokenStore: localStorageTokenStore(), // persists the customer session
});

Verify your setup

A built-in doctor checks reachability, the API key, origin allow-listing, and the data plane:

npx bowlos doctor --api-key "$NEXT_PUBLIC_BOWLOS_API_KEY" --base-url "https://<deployment>.convex.site/api/public/v1"

Quick start (React)

Wrap your app in the provider, then use the typed hooks:

import { BowlosProvider } from "@bowlos/reservations/react";
import { bowlos } from "./bowlos";

export function Providers({ children }: { children: React.ReactNode }) {
  return <BowlosProvider client={bowlos}>{children}</BowlosProvider>;
}
import { useAvailability } from "@bowlos/reservations/react";

function LaneAvailability() {
  const availability = useAvailability({
    productId: "<product_id>",
    dateISO: "2026-07-04",
    startMinuteOfDay: 18 * 60,
    endMinuteOfDay: 19 * 60,
  });
  if (availability.loading) return <p>Checking…</p>;
  return <p>{availability.data?.busyUnitIds.length ?? 0} units busy</p>;
}

Booking → pay → confirm

useBookingFlow orchestrates the hold → payment → confirmation state machine. It hands you the Stripe Connect PaymentIntent (clientSecret + connectedAccountId) — it does not load Stripe.js, so you render Stripe Elements yourself, scoped to the operator's connected account:

import { useBookingFlow } from "@bowlos/reservations/react";

const flow = useBookingFlow();
// flow.startHold(args)   → "held"
// flow.preparePayment()  → "awaitingPayment" (+ flow.paymentIntent)
// render Stripe Elements: loadStripe(pk, { stripeAccount: flow.paymentIntent.connectedAccountId })
// flow.startPolling()    → "confirmed"

A complete reference site (Next.js App Router) using this flow lives in the BowlOS repo under examples/center-site.

Buying passes, packages & memberships

Direct purchases — game passes, packages, and memberships — use usePurchase, the buy-flow twin of useBookingFlow: mint a connected-account PaymentIntent → pay with <BowlosPaymentForm> → poll to materialization. They require a signed-in customer and charge the operator's connected account.

import { useGameCardPurchase, usePackagePurchase, useMembershipPurchase } from "@bowlos/reservations/react";

const buy = useGameCardPurchase();          // or usePackagePurchase() / useMembershipPurchase()
// buy.start(args) → "awaitingPayment" (+ buy.paymentIntent)
// <BowlosPaymentForm {...buy.paymentIntent} onPaid={() => buy.startPolling()} />
// buy.status === "confirmed" → buy.resultId  (the new card / order / membership id)

A game pass is one-tap ({ templateId }) — show an itemized Review with useGameCardPreview (base, tax, booking fee, total) before paying, since the customer pays more than the bare priceCents. A package carries a config — Center, date, start time, quantity, add-ons — which you read with usePackage + useCenterHours, then show an itemized Review with usePackagePreview (base, add-ons, tax, booking fee, total). A one-time membership is one-tap like a game pass (useMembershipPurchase, Review with useMembershipPreview — dues + the one-time setup fee + tax + booking fee). A recurring membership uses useMembershipSubscriptionCheckout instead: its Review (useMembershipSubscriptionPreview) shows Due today and Auto-renews at, and there's no poll — it materializes via webhook, so re-query useMyMemberships after payment. All previews are server-computed and never diverge from the charge. The full recipes (including the package start-time slot snap and the recurring no-poll note) are in HEADLESS.md → "Buying a game pass" / "Buying a package" / "Buying a one-time membership" / "Buying a recurring membership".

Account + flash sales

Signed-in reads (require a customer session): useMyReservations, useMyMemberships, useMyLoyalty, useMyCredits, useMyPasses, useMyGameCards, plus details. Session-aware flash sales: useFlashSale, useFlashSaleBookingOptions. Manage the session with useAccount (signIn / signUp / link / signOut).

Self-serve cancel (v0.23.0): preview → confirm → cancel for reservations, event orders, package orders, and game cards — e.g. useGameCardCancelPreview

  • useCancelMyGameCard. Returns store credit per the Center's policy (card refunds stay staff-only); game cards cancel only while fully unused. See the "Self-serve cancel" section of HEADLESS.md.

Every read also has a framework-agnostic function in the core entry (checkAvailability, bookingPreview, getMyReservations, runDoctor, …) for non-React or server use.

Drop-in components (@bowlos/reservations/components)

These are a reference / fast-start, not the delivered product. Center sites are headless-first (your design, not a themed plugin) — build from the hooks per HEADLESS.md. Use these to prototype or to read how a behavior is wired.

Want a head start? A themeable, white-label component layer sits on top of the hooks:

import { BowlosProvider } from "@bowlos/reservations/react";
import { BookingFlow, BowlosTheme } from "@bowlos/reservations/components";

<BowlosProvider client={bowlos}>
  <BowlosTheme theme={{ colorPrimary: "#e4002b", radius: "8px" }}>
    <BookingFlow
      centerId="center_…"
      productId="product_…"
      pricingMode="per_person_per_hour" // count = Bowlers; per_unit_per_hour → Lanes; per_game → Games
      unitLabel={{ singular: "lane", plural: "lanes" }} // inventory noun on the "N available" line
      renderPayment={({ paymentIntent, onPaid, onError }) => (
        <YourStripeForm paymentIntent={paymentIntent} onPaid={onPaid} onError={onError} />
      )}
      onConfirmed={(r) => console.log("booked", r.reservationDisplayId)}
    />
  </BowlosTheme>
</BowlosProvider>;

<BookingCatalog brandId centerId onSelect> is the catalog landing — "what would you like to book?" Since v0.22.0 it renders the canonical visibility-aware catalog (useCatalog / listPublicCatalog) — the same read the white-label web and mobile surfaces use — so the Center admin's per-surface Public Catalog toggles apply to the drop-in too. Each pick hands off a CatalogItem via onSelect; route by item.action.type with the IDs in item.action.payload (same routing table as a headless site — HEADLESS.md → "The canonical catalog").

Date-first booking (v0.24.0): the catalog read now returns the Center's bookingEntryDefault (+ Center-local todayISO). <BookingCatalog> opens on that default and shows a "browse by activity / by date" toggle the customer can flip (showEntryToggle, or force with defaultEntry). Date-first lists what's bookable on a chosen day (per-product useStartTimes), an Events section, and an "Available any day" strip. Headless sites build the same from useCatalog + useStartTimes — see HEADLESS.md → "Date-first booking".

import { BookingCatalog } from "@bowlos/reservations/components";

<BookingCatalog
  brandId="brand_…"
  centerId="center_…"
  onSelect={(item) => {
    const p = item.action.payload;
    switch (item.action.type) {
      case "book_reservation": return router.push(`/book?productId=${p.productId}`);
      case "view_event":
      case "buy_ticket":       return router.push(`/events/${p.slug}`);
      case "buy_package":      return router.push(`/buy/package/${p.packageId}`);
      case "buy_membership":   return router.push(`/buy/membership/${p.templateId}`);
      case "buy_game_pass":    return router.push(`/buy/game-pass/${p.templateId}`);
    }
  }}
/>;

Breaking (v0.22.0): onSelect previously received a per-kind CatalogOffer union (o.kind / o.id); it now receives a CatalogItem. Section headings come from the backend (override with sectionLabels), and hidden categories/offers no longer render. Headless sites use the same data via useCatalog({ brandId, centerId, surface }) / listCatalog.

The drop-in surfaces cover a center site end to end:

<BookingFlow> runs the whole funnel (availability + price → review → pay → confirm). Online booking requires login — there is no anonymous path. A signed-out visitor sees a sign-in / create-account form (the built-in <AuthForm>); availability and pricing stay hidden until they authenticate, then the flow books as them (member rates apply). Anonymous booking is the walk-in / staff path only, never the public retail surface. It's pay-to-confirm by default (completeSaleOnly): the inventory hold advances straight to payment, so the customer never sees a held-but-unpaid reservation. It hands you the Stripe Connect PaymentIntent via renderPayment. Drop in the SDK's <BowlosPaymentForm> (see below) and you're done — or render your own Elements (loadStripe(pk, { stripeAccount: paymentIntent.connectedAccountId })confirmPayment → call onPaid()) for full control.

The date field defaults to today. The start time is a dropdown of real bookable slots — not a free-form clock — listing only the 30-min start times that fall within the center's operating hours, clear the booking buffer, and have an actual price plan (the same pricing engine the quote uses decides, so the dropdown never offers a slot the booking would reject). A closed or fully-gated date shows "No times available" and disables booking. The count field labels itself from pricingModeLanes (per_unit_per_hour), Bowlers (per_person_per_hour), Games (per_game) — and is sent to the API as the matching field (units / persons / games), so a per-bowler product books bowlers, not lanes. The page auto-scrolls to the top as the flow advances past the form.

The primary button reads "Review order" — submitting the form opens an order-summary review (the reservation + each add-on + the authoritative total) before anything is held or charged, matching the branded portal's select → review → pay flow. "Continue to payment" then places the hold and advances to payment; "Edit order" returns to the form. (Override copy via labels={{ reviewOrder, reviewHeading, orderTotal, editOrder, … }}.)

Add-ons (shoe rental, upgrades) render automatically: any add-ons configured on the product appear as a quantity-per-item list, each showing its unit price and rate (/ person, / unit, or flat). The customer's picks flow into both the live quote and the booking, so the price updates as add-ons are chosen and the server re-validates them against the live catalog. Nothing to wire — the component reads them from the product. (Need them headless? useAddons({ productId }) / listAddons(client, { productId }) return the typed PublicAddon[]; pass selections back as addons: [{ addonId, quantity }] on the preview + booking.)

<AccountDashboard brandId> is a complete account page: it signs the customer in (via the built-in <AuthGate>), then shows their reservations, memberships, loyalty, account credit, and passes. Pick a subset with sections={["reservations", "credits"]}.

import { AccountDashboard } from "@bowlos/reservations/components";

<AccountDashboard brandId="brand_…" />;

<FlashSale slug> is the branded, login-gated flash-sale page. Anonymous visitors see the brand, a live countdown, the operator's pre-login message, and item names with prices locked behind a sign-in form — sale prices are member-only and never cross the wire until a customer of the Brand signs in. Once signed in, prices unlock (shown as a per-date range) and each item gets a Book button you hand off via onBook:

import { FlashSale } from "@bowlos/reservations/components";

<FlashSale slug="tuesday-blowout" onBook={(item) => router.push(`/book?flashItem=${item.id}`)} />;

<FlashSaleIndex brandId> is the flash-sale landing page — the brand's live sales + upcoming scheduled ones (name + when purchases open), or a "check back soon" notice when there are none. No prices (those stay member-gated per sale). Open a live sale to route to its <FlashSale> page:

import { FlashSaleIndex } from "@bowlos/reservations/components";

<FlashSaleIndex brandId="brand_…" onOpen={(sale) => router.push(`/flash/${sale.slug}`)} />;

<AuthGate> (sign in / create account / finish profile) and <AccountMenu> (a header login indicator — the customer's first name + sign-out when signed in, a "Sign in" link otherwise) are exported on their own too. The customer session is shared app-wide by <BowlosProvider>, so useAccount() returns the same session everywhere — signing in or out on one page updates the header and the booking form on every page at once.

Payments (@bowlos/reservations/payments)

<BowlosPaymentForm> is the drop-in Stripe Connect checkout — wire it into <BookingFlow>'s renderPayment slot and you never hand-build a Stripe Elements form per site:

import { BookingFlow } from "@bowlos/reservations/components";
import { BowlosPaymentForm } from "@bowlos/reservations/payments";

<BookingFlow
  centerId="center_…"
  productId="product_…"
  renderPayment={(ctx) => (
    <BowlosPaymentForm {...ctx} publishableKey={process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!} />
  )}
/>;

It loads Stripe.js with your platform publishable key scoped to the operator's connected account (stripeAccount: connectedAccountId), renders the Payment Element, and confirms the PaymentIntent — the charge lands on the operator's Stripe account (their name on the statement/receipt). Saved cards + "save for future" are on by default (via the booking's customer session); pass saveCard={false} for a one-off charge. {...ctx} forwards the paymentIntent, amountCents, onPaid, and onError that renderPayment hands you. Theme the Payment Element to your site with the appearance prop (appearance={{ theme: "night", variables: { colorPrimary: "#FFD500" } }}) — Stripe's default is light.

This is the only entry that touches Stripe.js, so @stripe/stripe-js + @stripe/react-stripe-js are optional peers — install them only if you use this component; the core / React / components bundles never pull Stripe:

npm install @stripe/stripe-js @stripe/react-stripe-js

Prefer full control? Skip this and render your own Elements in renderPayment.

Theming: neutral defaults with no BowlOS branding (these are your center's pages). Restyle via <BowlosTheme> or by setting the --bowlos-* CSS variables yourself (--bowlos-color-primary, --bowlos-radius, --bowlos-font, …).

Errors

Failed calls throw a typed BowlosApiError with a stable .code — branch on that, never on message text:

import { BowlosApiError } from "@bowlos/reservations";

try {
  await bowlos.signIn(email, password);
} catch (e) {
  if (e instanceof BowlosApiError && e.code === "INVALID_CREDENTIALS") {
    // show "wrong email or password"
  }
}

License

MIT