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

@backendfree/bookings

v0.1.1

Published

Typed access to a BackendFree project's diary: services, staff, availability and booking.

Readme

@backendfree/bookings

A project's diary: what it sells, who does it, when they are free, and booking it.

npm install @backendfree/core @backendfree/bookings
import { Client } from '@backendfree/core';
import { Bookings, localTime } from '@backendfree/bookings';

const bookings = new Bookings(new Client({ origin: 'https://backendfree.com', key: 'pk_live_...' }));

const { results: services } = await bookings.services();
const { slots, timezone } = await bookings.availability({
  service: services[0].id,
  from: '2026-06-10',
});

const booking = await bookings.book({
  service: services[0].id,
  starts_at: slots[0].start,
  customer_email: '[email protected]',
  success_url: 'https://salon.example/thanks',
  cancel_url: 'https://salon.example/book',
});

A publishable key is enough to book

Taking a booking from a browser is the product, and what bounds it is what one customer can do rather than who holds the key.

Reading the diary is the exception. list() needs a secret key, because it is every customer's name, address and where they will be on Thursday.

When a time was just taken

Free times are cached for up to a minute, so one can be gone by the time it is booked. book() then throws with the code slot_unavailable, and nothing was booked. Asking availability() again straight away can offer the same time for the rest of that minute, so stop showing the one that was refused:

import { BackendFreeError } from '@backendfree/core';

try {
  await bookings.book(request);
} catch (error) {
  if (error instanceof BackendFreeError && error.code === 'slot_unavailable') {
    refused.add(request.starts_at);   // and leave it out of what you show
  }
}

Two people can never hold one time: the booking call is what decides, not the list of free times.

The manage token

book() returns manage_token exactly once. It is a capability: whoever holds it may read, cancel or reschedule that one booking and nothing else. Keep it if the customer will manage their own appointment from a browser, and treat it like a password.

await bookings.cancel(booking.id, { token: booking.manage_token, reason: 'Away' });
await bookings.reschedule(booking.id, { token, starts_at: otherSlot.start });

No later call hands it over again, so a listing cannot leak one.

The manage link

The confirmation email can link to a page on your site where the customer reads, moves or cancels their booking. On the booking rules screen, set Where customers manage their booking to an https address holding {token}, and usually {id}:

https://salon.example/booking/{id}?token={token}

Both are filled in for each booking. That page reads the booking with them:

const booking = await bookings.booking(id, { token });

With the field empty, the email carries the details and no link.

Paying

A booking that has to be paid for comes back pending_payment with somewhere to send the customer:

if (booking.payment?.required) {
  window.location.href = booking.payment.checkout_url;
}

Nothing confirms a booking except the provider's webhook. The page the customer lands on afterwards proves they got back to your site, not that they paid. Poll booking() or listen for your own webhook.

{id} in success_url or cancel_url becomes the booking's id, so that page knows which booking to read: success_url: 'https://salon.example/thanks?booking={id}'. Only the id: the manage token is never put in an address, so keep it from book() if the page has to read the booking with a publishable key.

The slot is held only until payment.expires_at. If the payment page could not be opened, or the customer closed the tab, checkout() offers it again:

const withPage = await bookings.checkout(booking.id, { token, success_url, cancel_url });

Deliberately not a retry of book(): booking again would ask for a second appointment in a slot the first one is already holding.

Times

Send instants with an offset. A time with no zone is refused at the boundary, because the browser's clock, the server's and the business's are three different answers and a naive time silently picks one.

Read them on the business's clock, which availability() tells you:

localTime(slot.start, timezone);                       // ten in the morning, the reader's way
localTime(slot.start, timezone, undefined, 'de-CH');   // '10:00' for a Bern salon

The zone is the business's and the wording is the reader's. Ten o'clock in Bern is ten o'clock however it is written.

Money

Always { amount, currency } in minor units: { amount: 6500, currency: 'CHF' } is CHF 65.00. Never divide by a hundred without checking the currency, and never put it in a float.

Rules

settings() publishes every rule the create call enforces, so a form can grey out what would be refused rather than finding out afterwards: how far ahead bookings are taken, how close to the appointment, the grid times sit on, whether a phone number is wanted, and whether customers may move their own booking.