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

@strmlsys/booking-engine

v1.1.0

Published

Reusable booking engine that powers app.lumiagenda.co/book and every Tier 2 custom client website. Same code, one source of truth.

Readme

@strmlsys/booking-engine

Reusable booking engine that powers Spaboard's hosted booking page AND every Tier 2 custom client website. One source of truth — bugfixes and features propagate to every consumer.

Install

Consumed straight from this public repo as an HTTPS tarball — it is not published to npm, and you should not use the github: shorthand (see the warning below). Add it to the host app's package.json:

{
  "dependencies": {
    "@strmlsys/booking-engine": "https://github.com/restreposimon/booking-engine/archive/refs/heads/main.tar.gz",
    "react": "^18 || ^19",
    "react-dom": "^18 || ^19"
  }
}

…then npm install (or pnpm install / yarn).

⚠️ Do NOT use github:restreposimon/booking-engine#main. npm rewrites github: / git dependencies to git+ssh://[email protected]/… in the lockfile, which fails on Vercel and most CI with Permission denied (publickey) — the build machine has no SSH key. The HTTPS tarball above is fetched anonymously: no SSH key, no token, no vercel.json, nothing to configure.

Required: transpile the package

The engine ships raw TypeScript (no build step), so the host app must transpile it like first-party code. In Next.js, add to next.config.mjs:

const nextConfig = { transpilePackages: ["@strmlsys/booking-engine"] }
export default nextConfig

Skip this and the build fails trying to parse .tsx from node_modules.

Deploying (Vercel / any host)

No deploy config is needed — the public tarball installs anonymously during the build. The one server-side prerequisite is the CORS allowlist: the site's domain must be registered for the business on the Spaboard side, or the API returns 403 to cross-origin calls. If the page loads but data fails with a CORS / 403 error, that's the signal — the Spaboard operator adds the domain (e.g. https://clientsite.com, plus http://localhost:3000 for local dev). This is a server-side allowlist entry, not a client-code fix — do not try to work around it in the app.

Quick start

import { BookingFlow } from "@strmlsys/booking-engine"

export default function ReservarPage() {
  return (
    <BookingFlow
      businessId="casaverde"
      apiUrl="https://app.lumiagenda.co/api/public/v1"
    />
  )
}

That's it. The component handles service selection, calendar, time slots, customer form, gift cards, payment redirect to Wompi, and the result page handoff.

The three drop-in flows

<BookingFlow> is the main entry point, but the engine ships two sibling components for the other two pages a booking site needs. All three take the same businessId / apiUrl / theme / locale props, share the same security model, and render fully in both Spanish and English.

| Component | Renders | Typical route | |---|---|---| | <BookingFlow> | Service list → date → time → customer → payment | /book/[slug] | | <GiftCardFlow> | Gift-card page: buy a card, gift a card, or gift a service | /book/[slug]/giftcards | | <PaymentResultFlow> | Post-Wompi success / pending / error screen | /book/[slug]/result |

import { GiftCardFlow, PaymentResultFlow } from "@strmlsys/booking-engine"

// Gift-card purchase page
<GiftCardFlow businessId="casaverde" />

// The page Wompi redirects back to after payment (appointments AND gift cards)
<PaymentResultFlow businessId="casaverde" />

<GiftCardFlow> also accepts an optional testMode prop (routes purchases through Wompi's sandbox) plus bookingUrl / giftCardRedirectUrl overrides.

<PaymentResultFlow> reads the Wompi status straight from the URL query string, so it needs no server-side data and stays fast even if the API is slow — it only fires a best-effort saveTransaction call (the signed webhook is the real source of truth, so a failure here never blocks the success screen).

Configuration

Required props

| Prop | Type | Description | |---|---|---| | businessId | string | The slug of the business this flow books for. Must exist in Spaboard's DB. | | apiUrl | string | Base URL of Spaboard's public v1 API. May also be supplied via NEXT_PUBLIC_SPABOARD_API_URL. |

Optional props

| Prop | Type | Description | |---|---|---| | theme | PartialBookingTheme | Visual tokens — colors, fonts, radius. Missing keys fall back to defaults. | | locale | "es" \| "en" | UI language. Defaults to "es". | | paymentRedirectUrl | string | Where Wompi redirects after payment. Defaults to ${origin}/book/${businessId}/result?appointment_id=.... Tier 2 sites should point this at their own result page. | | onBuyGiftCard | () => void | Click handler for the "Buy a gift card" link. If omitted, the link is hidden. | | loadingFallback | ReactNode | Custom loading UI while the business is being fetched. | | errorFallback | (error) => ReactNode | Custom error UI. |

Environment variables

If you set these on the host site, they're picked up automatically:

NEXT_PUBLIC_SPABOARD_API_URL=https://app.lumiagenda.co/api/public/v1
NEXT_PUBLIC_BUSINESS_SLUG=casaverde   # optional convention; you still pass it via businessId prop

Both values are public, safe to commit. No secrets ever live in a Tier 2 client site — Spaboard's server holds every key.

Theming

<BookingFlow
  businessId="casaverde"
  theme={{
    primary: "#2D5016",
    background: "#f5f5dc",
    surface: "#ffffff",
    text: "#1a1a1a",
    muted: "rgba(0,0,0,0.55)",
    border: "rgba(0,0,0,0.1)",
    primaryForeground: "#ffffff",
    fontDisplay: "'Cormorant Garamond', serif",
    fontBody: "system-ui, sans-serif",
    radius: "4px",
  }}
/>

Themes are applied as CSS custom properties on a wrapper element (--be-primary, --be-radius, etc.). The engine's components reference those vars, so a single wrapper styles the entire flow.

Localization

<BookingFlow businessId="casaverde" locale="en" />

Two locales ship today: "es" (Spanish, default) and "en" (English). Every user-facing string is translated — across all three flows: the calendar and time picker, the multi-step booking modal, the payment summary and policies, the gift-card purchase modal, the result screen, and the sandbox test-mode banner. There are no hardcoded strings in any locale.

Add more by translating src/i18n/es.ts → src/i18n/<locale>.ts and registering in src/i18n/index.ts. es.ts is the as const source of truth; every other locale's type is checked against it, so a missing key is a compile error.

Architecture

The engine is just React + a thin SDK. No state management, no global context — drop it into any React app, server-rendered or client-rendered, Next.js or otherwise.

@strmlsys/booking-engine
├── src/
│   ├── components/
│   │   ├── BookingFlow.tsx          # top-level booking — fetches business, themes, renders BookingPage
│   │   ├── BookingPage.tsx          # service list + landing
│   │   ├── BookingSteps.tsx         # inline multi-step flow (services, date, time, customer, payment)
│   │   ├── GiftCardForm.tsx         # gift-card purchase form (inline on GiftCardPage, wrapped by GiftCardModal)
│   │   ├── ui/                      # package-local kit: icons, primitives (reads --be-* theme vars)
│   │   ├── BookingProgress.tsx      # progress indicator
│   │   ├── PaymentStep.tsx          # Wompi redirect + summary
│   │   ├── GiftCardFlow.tsx         # top-level gift-card — fetches business, renders GiftCardPage
│   │   ├── GiftCardPage.tsx         # gift-card page shell (Stage + hosted chrome)
│   │   ├── GiftCardSteps.tsx        # choose (self / gift / service), form, Wompi handoff
│   │   ├── GiftCardModal.tsx        # multi-step gift-card purchase modal
│   │   ├── PaymentResultFlow.tsx    # top-level result screen (themed wrapper, no fetch)
│   │   ├── PaymentResult.tsx        # post-Wompi success / pending / error UI
│   │   ├── PublicHeader.tsx         # branding strip
│   │   ├── ServiceCard.tsx          # service preview
│   │   └── adapters.ts              # Business/Service → internal PublicSpa/PublicService
│   ├── sdk/                         # createClient() + typed methods
│   ├── i18n/                        # es.ts, en.ts, getTranslations()
│   ├── theme/                       # default tokens + resolveTheme + themeToCssVars
│   ├── types/                       # Business, Service, etc.
│   └── utils/                       # currency formatting, interpolation
└── package.json

Talking directly to the API

For very custom flows where the default UI doesn't fit, use the SDK:

import { createClient } from "@strmlsys/booking-engine"

const client = createClient({
  apiUrl: "https://app.lumiagenda.co/api/public/v1",
  businessId: "casaverde",
})

const { business, services } = await client.getBusiness()
const { dates } = await client.getAvailableDates({ serviceId, startDate, endDate })
const result = await client.createAppointment({ ... })

Every method is typed; errors throw ApiError with status, code, message, details.

Booking verification (bookings with no online payment)

A booking that takes no online payment (and is not fully covered by a gift card) needs a verification code first. createAppointment then throws ApiError with code verification_required and creates nothing. The built-in flows handle this; a custom flow does:

const challenge = await client.requestVerification({ phone, email })
// challenge.channel: "sms" for Colombian mobiles, "email" for any other
// number (then `email` is required). challenge.destination is masked.
const { verificationToken } = await client.verifyCode({
  challengeId: challenge.challengeId,
  code, // 6 digits typed by the customer
})
await client.createAppointment({ ...input, verificationToken })

The token lasts 30 minutes and covers up to 3 bookings for the same phone. Error codes: invalid_code (details.attemptsLeft), expired, rate_limited, verification_locked, verification_unavailable, email_required, invalid_email, disposable_email, invalid_phone. With testMode the code is always 000000 and nothing is sent.

Marketing consent (promotions checkbox)

The built-in booking flow shows an optional checkbox after the contact fields, unticked by default: "Quiero recibir promociones y novedades de {negocio} por WhatsApp, SMS y correo." A custom flow that shows the same checkbox passes the customer's choice:

await client.createAppointment({ ...input, marketingConsent: true })

Rules (Ley 1581 de 2012, Decreto 1377 de 2013):

  • Never pre-tick it and never make it required.
  • Use exactly that Spanish text with the business name: the API stores it as the proof of consent, together with a hashed IP.
  • true records a grant for that customer and business, covering WhatsApp, SMS and email. false or omitted records nothing and never revokes an earlier opt-in (clients opt out by replying to a WhatsApp or from the business).
  • Consent is recorded only once the booking is confirmed: right away for a booking with no online payment (after the verification code), and when the payment confirms for a booking that goes to Wompi (an abandoned or declined checkout records nothing). A failure to record it never fails the booking. Calls from the hosted Lumi page are logged with source booking_page, calls from your own site or server with source api.

Documents (forms and waivers)

A business can attach forms and waivers to a service. The createAppointment response then carries a personal link for the client:

"documents": { "required": true, "url": "https://lumiagenda.co/d/<token>", "count": 2, "available": false }

documents is null when the service requires nothing. available is false while an online payment is pending (the link opens locked until it confirms).

The built-in flows already handle it: the url is kept in sessionStorage with the booking (same tab, keyed by appointment id), and once the booking is confirmed (right after a booking with no online payment, or on the payment result page once Wompi approves) the done screen shows "Un paso más: completa tus documentos" and redirects there after 2 seconds.

A custom flow should do the same with the url from the response. To decide when to show its own "Completa tus documentos" button, poll the status endpoint, which never returns the url:

GET /api/public/v1/businesses/:slug/appointments/:id/documents
-> { "required": true, "count": 2, "completed": 0, "available": true }

Keep the url private (it is the client's only credential for their documents). If the client drops off, the confirmation email and WhatsApp carry the same link.

Lower-level components

Power users can compose with the sub-components instead of <BookingFlow>:

import { BookingPage, createClient } from "@strmlsys/booking-engine"

const client = createClient({ apiUrl, businessId })
const { business, services, wompi_public_key } = await client.getBusiness()

<BookingPage
  slug={businessId}
  spa={...}  // map business to PublicSpa shape
  services={...}
  client={client}
  wompiPublicKey={wompi_public_key}
  translations={getTranslations("es")}
/>

This is the same path Spaboard's own /book/[slug] page uses (server-side data loading + client component rendering).

Security model

The engine holds zero secrets — no API keys, no database credentials, nothing. It only ever calls Spaboard's hosted public API, and every security decision is made server-side: authentication, authorization, input validation, abuse/bot protection, rate limiting, and payment verification all live on Spaboard's servers, never in this package.

Because a Tier 2 site carries no secrets and makes no trust decisions of its own, there's nothing sensitive to leak in its source. The specifics of the server-side protections are documented privately, not here.

Updating

There are no version numbers to bump — every site installs from the main tarball, so npm install always pulls the latest engine code. To ship an engine update to a client site, reinstall and redeploy (CI reinstalls on every deploy):

npm install   # re-fetches the main tarball

Because the engine is the single source of truth, a fix made once in this repo reaches every site on its next install — no per-site code changes. (The repo is mirrored automatically from the Spaboard monorepo; never edit it directly, your changes get overwritten on the next sync.)

License

Proprietary — STRMLSYS. Not for redistribution.