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

@nineteenlabs/quickschedule

v0.1.0

Published

Open-source scheduling plugin for Payload CMS 3

Readme

@nineteenlabs/quickschedule

A scheduling plugin for Payload CMS 3 that adds appointment booking to any Payload project. Drop in 4 collections, wire up API routes, and build your own booking UI with headless React components.

Features

  • 4 collections — providers, appointment types, bookings, blocked times
  • Slot computation — intersects provider schedules with bookings and blocked times
  • Headless React components — render-prop based, bring your own styles
  • Email notifications — confirmation, cancellation, reminders (Resend, custom, or console)
  • Fully configurable — custom slugs, extra fields, validation hooks, timezone support
  • TypeScript-first — complete type definitions for everything

Quick Start

Install

npm install @nineteenlabs/quickschedule
# or
pnpm add @nineteenlabs/quickschedule

Peer dependencies: payload@^3.0.0, react@^18 || ^19

Add the Plugin

// payload.config.ts
import { buildConfig } from 'payload'
import { quickschedulePlugin } from '@nineteenlabs/quickschedule'

export default buildConfig({
  plugins: [
    quickschedulePlugin({
      slotInterval: 30,       // minutes between slots (default: 30)
      timezone: 'America/New_York',
    }),
  ],
  // ... rest of your config
})

This adds 4 collections to your Payload admin:

  • qs-providers — staff/practitioners with weekly schedules
  • qs-appointment-types — services offered (duration, pricing, limits)
  • qs-bookings — individual appointments
  • qs-blocked-times — time-off, holidays, blocked periods

Wire Up API Routes

Create a catch-all route in your Next.js app:

// app/api/quickschedule/[...path]/route.ts
import { createRouteHandlers } from '@nineteenlabs/quickschedule/api'
import configPromise from '@payload-config'

const { GET, POST } = createRouteHandlers(configPromise)
export { GET, POST }

This exposes:

| Method | Endpoint | Description | |--------|----------|-------------| | GET | /api/quickschedule/providers | List active providers | | GET | /api/quickschedule/appointment-types | List appointment types (optional ?providerId=) | | GET | /api/quickschedule/availability | Get available slots (?providerId=&appointmentTypeId=&date=) | | POST | /api/quickschedule/book | Create a booking | | POST | /api/quickschedule/cancel | Cancel a booking |

Add the Booking UI

Wrap your booking page with BookingProvider and compose the headless components:

'use client'

import {
  BookingProvider,
  AppointmentTypePicker,
  DatePicker,
  TimeSlotPicker,
  BookingForm,
  BookingConfirmation,
} from '@nineteenlabs/quickschedule/react'

export default function BookingPage() {
  return (
    <BookingProvider>
      <h1>Book an Appointment</h1>
      <AppointmentTypePicker />
      <DatePicker />
      <TimeSlotPicker />
      <BookingForm />
      <BookingConfirmation />
    </BookingProvider>
  )
}

That's it. The components manage the booking flow automatically: select type, pick date, choose time, fill form, see confirmation.

Configuration

All options with their defaults:

quickschedulePlugin({
  // Scheduling
  slotInterval: 30,          // minutes between available slots
  bookingWindow: 60,         // how many days ahead bookings are allowed
  minNotice: 24,             // minimum hours before appointment
  timezone: 'UTC',           // IANA timezone

  // Enable/disable
  enabled: true,

  // API
  routePrefix: '/api/quickschedule',

  // Validation
  validation: {
    requirePhone: false,
    requireEmail: true,      // true by default
    customValidation: async (booking) => ({
      valid: true,
      message: undefined,
    }),
  },

  // Email
  email: {
    provider: 'ses',         // 'ses' | 'resend' | 'console' | 'custom'
    region: 'us-east-1',     // AWS region (for SES)
    apiKey: '',              // API key (for Resend)
    from: '[email protected]',
    replyTo: '[email protected]',
    templates: {},            // override default templates
    variables: {},            // extra template variables
  },

  // Notifications
  notifications: {
    sendConfirmation: true,
    sendReminder: false,
    reminderHours: 24,
    notifyProvider: false,
    notifyEmail: '',          // office email for notifications
  },

  // Collection overrides
  collections: {
    providers:        { slug: 'qs-providers' },
    appointmentTypes: { slug: 'qs-appointment-types' },
    bookings:         { slug: 'qs-bookings' },
    blockedTimes:     { slug: 'qs-blocked-times' },
  },
})

Custom Collection Slugs

Rename any collection to fit your project:

quickschedulePlugin({
  collections: {
    providers: { slug: 'staff' },
    bookings: { slug: 'appointments' },
  },
})

Extra Fields

Add custom fields to any collection:

quickschedulePlugin({
  collections: {
    providers: {
      fields: [
        { name: 'specialty', type: 'text' },
        { name: 'languages', type: 'select', options: ['English', 'Spanish'] },
      ],
    },
    bookings: {
      fields: [
        { name: 'insuranceProvider', type: 'text' },
      ],
    },
  },
})

React Components

All components are headless — they handle data fetching and state, you control rendering through render props. Every component works out of the box with sensible defaults, but you can customize everything.

BookingProvider

Wraps your booking UI and manages the multi-step flow.

<BookingProvider
  apiBase="/api/quickschedule"           // API route prefix
  onBookingComplete={(response) => {}}   // called on successful booking
  onError={(error) => {}}                // called on errors
>
  {children}
</BookingProvider>

Flow steps: typedatetimeformconfirmation

AppointmentTypePicker

Fetches and displays available appointment types.

<AppointmentTypePicker
  providerId="optional-filter"
  renderType={({ type, isSelected, select }) => (
    <div
      onClick={select}
      className={isSelected ? 'selected' : ''}
    >
      <h3>{type.name}</h3>
      <p>{type.duration} minutes — ${type.price}</p>
    </div>
  )}
  renderLoading={() => <Spinner />}
  renderEmpty={() => <p>No appointment types available</p>}
/>

DatePicker

A calendar date picker with month navigation.

// Custom rendering
<DatePicker
  renderDay={({ date, dateStr, isSelected, isToday, isPast, select }) => (
    <button
      onClick={select}
      disabled={isPast}
      className={cn({ selected: isSelected, today: isToday })}
    >
      {date.getDate()}
    </button>
  )}
  renderHeader={({ month, year, prev, next }) => (
    <div className="calendar-header">
      <button onClick={prev}>←</button>
      <span>{month} {year}</span>
      <button onClick={next}>→</button>
    </div>
  )}
/>

// Or delegate to a third-party date picker
<DatePicker
  as={({ value, onChange }) => (
    <MyFancyDatePicker value={value} onChange={onChange} />
  )}
/>

TimeSlotPicker

Shows available time slots for the selected provider, type, and date.

<TimeSlotPicker
  renderSlot={({ time, isSelected, select }) => (
    <button
      onClick={select}
      className={isSelected ? 'ring-2 ring-blue-500' : ''}
    >
      {time}
    </button>
  )}
  renderLoading={() => <p>Finding available times...</p>}
  renderEmpty={() => <p>No times available for this date</p>}
/>

BookingForm

Patient information form with validation.

<BookingForm
  fields={[
    { name: 'name', label: 'Full Name', required: true },
    { name: 'email', label: 'Email', type: 'email', required: true },
    { name: 'phone', label: 'Phone', type: 'tel' },
    { name: 'notes', label: 'Anything we should know?', type: 'textarea' },
  ]}
  renderField={({ field, value, onChange, error }) => (
    <div>
      <label>{field.label}</label>
      <input
        type={field.type || 'text'}
        value={value}
        onChange={(e) => onChange(e.target.value)}
      />
      {error && <span className="text-red-500">{error}</span>}
    </div>
  )}
  renderSubmit={({ isSubmitting, submit }) => (
    <button onClick={submit} disabled={isSubmitting}>
      {isSubmitting ? 'Booking...' : 'Confirm Booking'}
    </button>
  )}
  onSuccess={() => console.log('Booked!')}
/>

BookingConfirmation

Renders after a successful booking.

<BookingConfirmation
  render={({ booking, provider, appointmentType }) => (
    <div>
      <h2>You're booked!</h2>
      <p>Confirmation: {booking.confirmationNumber}</p>
      <p>{appointmentType.name} with {provider.name}</p>
      <p>{booking.date} at {booking.time}</p>
    </div>
  )}
/>

Hooks

Use hooks directly for custom flows:

import { useBookingContext, useAvailableSlots, useCreateBooking } from '@nineteenlabs/quickschedule/react'

// Access the full booking state
const { step, selectedType, selectDate, reset } = useBookingContext()

// Fetch available slots (reads provider/type/date from context)
const { slots, isLoading, error, refetch } = useAvailableSlots()

// Create a booking (reads selections from context)
const { book, isBooking, error } = useCreateBooking()
const result = await book({ name: 'Jane', email: '[email protected]', phone: '555-0123' })

API Reference

Booking Request

POST /api/quickschedule/book
Content-Type: application/json

{
  "appointmentTypeId": "abc123",
  "providerId": "def456",
  "date": "2026-03-16",
  "time": "10:00",
  "patient": {
    "name": "Jane Smith",
    "email": "[email protected]",
    "phone": "555-123-4567",
    "notes": "First visit"
  }
}

Success (201):

{
  "success": true,
  "booking": {
    "id": "booking-id",
    "confirmationNumber": "QS-2026-0316-001",
    "date": "2026-03-16",
    "time": "10:00",
    "duration": 30,
    "status": "confirmed"
  },
  "token": "cancel-token"
}

Error (400):

{
  "success": false,
  "error": "slot_unavailable",
  "message": "This time slot is no longer available"
}

Error codes: validation_error, provider_not_found, appointment_type_not_found, slot_unavailable

Cancel Request

POST /api/quickschedule/cancel
Content-Type: application/json

{
  "bookingId": "booking-id",
  "token": "cancel-token",
  "reason": "Schedule conflict"
}

Availability Query

GET /api/quickschedule/availability?providerId=xxx&appointmentTypeId=xxx&date=2026-03-16
{
  "date": "2026-03-16",
  "provider": { "id": "xxx", "name": "Dr. Smith" },
  "appointmentType": { "id": "xxx", "name": "Consultation", "duration": 30 },
  "availableSlots": ["09:00", "09:30", "10:30", "11:00", "14:00", "14:30"]
}

Email Templates

Four built-in templates use {{variable}} interpolation:

| Template | Sent When | Recipient | |----------|-----------|-----------| | confirmation | Booking created | Patient | | cancellation | Booking cancelled | Patient | | reminder | Before appointment | Patient | | providerNotification | Booking created | Provider/office |

Supported providers:

| Provider | Config | Install | |----------|--------|---------| | ses | region (default: us-east-1) | npm install @aws-sdk/client-ses | | resend | apiKey | included (uses fetch) | | console | none | included (logs to stdout) | | custom | sendFn | bring your own |

SES uses your standard AWS credential chain (env vars, IAM role, ~/.aws/credentials).

Override any template:

quickschedulePlugin({
  email: {
    provider: 'ses',
    region: 'us-east-1',
    from: '[email protected]',
    templates: {
      confirmation: {
        subject: 'Your appointment is confirmed, {{patientName}}!',
        html: `
          <h1>See you soon!</h1>
          <p>You have a {{appointmentTypeName}} with {{providerName}}
             on {{formattedDate}} at {{formattedTime}}.</p>
          <p>Confirmation: {{confirmationNumber}}</p>
          <p><a href="{{cancelUrl}}">Need to cancel?</a></p>
        `,
      },
    },
  },
})

Available variables: patientName, patientEmail, patientPhone, providerName, appointmentTypeName, date, formattedDate, time, formattedTime, duration, confirmationNumber, cancelUrl

Using Without React

The availability and booking logic works standalone:

import { getAvailableSlots, processBooking } from '@nineteenlabs/quickschedule/api'

// Pure function — compute available slots
const availability = getAvailableSlots({
  provider,
  appointmentType,
  date: '2026-03-16',
  bookings: existingBookings,
  blockedTimes: blockedTimes,
  config: { slotInterval: 30, minNotice: 24, timezone: 'UTC' },
})

// With dependency injection for testability
const result = await processBooking(bookingInput, {
  findProvider: async (id) => { /* your lookup */ },
  findAppointmentType: async (id) => { /* your lookup */ },
  findBookingsForDate: async (providerId, date) => { /* your query */ },
  findBlockedTimesForDate: async (providerId, date) => { /* your query */ },
  createBookingRecord: async (data) => { /* your insert */ },
  countBookingsForDate: async (providerId, date) => { /* your count */ },
  config: resolvedConfig,
})

Types

Import types from the main package or the dedicated types entry:

import type {
  Provider,
  AppointmentType,
  Booking,
  BlockedTime,
  BookingInput,
  BookingResponse,
  AvailabilityResponse,
  QuickScheduleConfig,
} from '@nineteenlabs/quickschedule'

// or
import type { Provider } from '@nineteenlabs/quickschedule/types'

License

MIT