Skip to content

Server Reference (@thebookingkit/server)

@thebookingkit/server provides server-side utilities for Next.js API routes: authentication middleware, pluggable adapters, webhook signing, API key management, booking tokens, multi-tenancy helpers, and workflow evaluation.

Terminal window
npm install @thebookingkit/server

Middleware wrapper that injects the authenticated user into every request handler. Rejects unauthenticated requests with HTTP 401. Optionally enforces a minimum role.

app/api/bookings/route.ts
import { withAuth } from "@thebookingkit/server";
export const GET = withAuth(authAdapter, async (req) => {
// req.user is fully typed as AuthUser
const bookings = await db.query.bookings.findMany({
where: (b, { eq }) => eq(b.providerId, req.user.id),
});
return Response.json(bookings);
});
// With required role
export const DELETE = withAuth(
authAdapter,
async (req) => { /* ... */ },
{ requiredRole: "admin" },
);

Parameters

ParameterTypeDescription
adapterAuthAdapterAuth adapter instance
handler(req: AuthenticatedRequest) => Promise<Response>Request handler with injected user
options.requiredRole"admin" | "provider" | "member" | "customer"Minimum role required (uses hierarchy: admin > provider > member > customer)

Error responses

ConditionHTTP statusError code
No session and no Bearer token401UNAUTHORIZED
Insufficient role403FORBIDDEN
Booking conflict inside handler409BOOKING_CONFLICT
Resource unavailable inside handler409RESOURCE_UNAVAILABLE

Throws ForbiddenError if the authenticated user’s ID does not match the resource owner’s user ID. Use to scope provider data access.

import { assertProviderOwnership } from "@thebookingkit/server";
assertProviderOwnership(req.user.id, provider.userId);
// throws ForbiddenError if IDs do not match

Parameters

ParameterTypeDescription
userIdstringAuthenticated user’s ID
resourceUserIdstringOwner ID on the resource being accessed

Throws ForbiddenError if the authenticated user’s email does not match the booking’s customer email.

import { assertCustomerAccess } from "@thebookingkit/server";
assertCustomerAccess(req.user.email, booking.customerEmail);

Parameters

ParameterTypeDescription
userEmailstringAuthenticated user’s email
bookingCustomerEmailstringCustomer email on the booking

The AuthAdapter interface is the contract for pluggable authentication backends. Implement it to integrate any auth provider.

import type { AuthAdapter, AuthUser, AuthSession } from "@thebookingkit/server";
class MyAuthAdapter implements AuthAdapter {
async getCurrentUser(request: Request): Promise<AuthUser | null> {
// resolve session cookie, return user or null
}
async getSession(request: Request): Promise<AuthSession | null> {
// return full session including expiry
}
async verifyToken(token: string): Promise<AuthUser | null> {
// validate API token or signed booking token
}
}

AuthUser fields

FieldTypeDescription
idstringUnique user identifier
emailstringUser email address
namestring (optional)Display name
role"admin" | "provider" | "member" | "customer" (optional)User role

AuthSession fields

FieldTypeDescription
userAuthUserThe authenticated user
expiresDateSession expiry time

Default implementation: NextAuth.js. Alternatives: Supabase Auth, Clerk, Lucia.


Signed, time-limited tokens that let customers view or manage their booking without a full auth session. The token embeds the booking ID and expiry, signed with HMAC-SHA256.

import { generateBookingToken } from "@thebookingkit/server";
const token = generateBookingToken(
booking.id,
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
process.env.BOOKING_TOKEN_SECRET!,
);
// => base64url-encoded signed token

Parameters

ParameterTypeDescription
bookingIdstringThe booking UUID
expiresAtDateToken expiry time
secretstringHMAC signing secret

Returns string — base64url-encoded token safe for use in URLs.

import { verifyBookingToken } from "@thebookingkit/server";
const result = verifyBookingToken(token, process.env.BOOKING_TOKEN_SECRET!);
if (!result) {
return new Response("Invalid or expired token", { status: 401 });
}
const { bookingId, expiresAt } = result;

Parameters

ParameterTypeDescription
tokenstringThe base64url-encoded token
secretstringHMAC signing secret (must match generation)

Returns { bookingId: string; expiresAt: Date } | nullnull if the token is invalid, malformed, or expired. Uses constant-time comparison to prevent timing attacks.


HMAC-SHA256 signed webhook delivery with replay protection.

import { signWebhookPayload, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "@thebookingkit/server";
const timestampSeconds = Math.floor(Date.now() / 1000);
const rawBody = JSON.stringify(envelope);
const signature = signWebhookPayload(rawBody, webhookSecret, timestampSeconds);
// Add to delivery headers:
// X-BookingKit-Signature: <signature>
// X-BookingKit-Timestamp: <timestampSeconds>

Parameters

ParameterTypeDescription
rawBodystringRaw JSON string of the payload
secretstringWebhook secret key
timestampSecondsnumberUnix timestamp in seconds

Returns string — hex-encoded HMAC-SHA256 signature. Signed message is ${timestamp}.${rawBody}.

import { verifyWebhookSignature } from "@thebookingkit/server";
const result = verifyWebhookSignature(
rawBody,
req.headers.get("X-BookingKit-Signature")!,
Number(req.headers.get("X-BookingKit-Timestamp")),
webhookSecret,
{ toleranceSeconds: 300 }, // default: 5 minutes
);
if (!result.valid) {
return Response.json({ error: result.reason }, { status: 401 });
}

Parameters

ParameterTypeDescription
rawBodystringRaw JSON string of the received payload
signaturestringValue of the X-BookingKit-Signature header
timestampSecondsnumberValue of the X-BookingKit-Timestamp header
secretstringWebhook secret key
options.toleranceSecondsnumberMaximum timestamp age in seconds. Default: 300

Returns WebhookVerificationResult{ valid: true } or { valid: false, reason: "timestamp_expired" | "signature_mismatch" }.

Constructs the standard webhook envelope object.

import { createWebhookEnvelope } from "@thebookingkit/server";
const envelope = createWebhookEnvelope("BOOKING_CONFIRMED", {
bookingId: booking.id,
eventType: eventType.title,
startTime: booking.startsAt.toISOString(),
endTime: booking.endsAt.toISOString(),
organizer: { name: provider.name, email: provider.email },
attendees: [{ email: booking.customerEmail, name: booking.customerName }],
status: booking.status,
});

Filters a list of subscriptions to those that are active and subscribed to the given trigger, with optional event type or team scope.

import { matchWebhookSubscriptions } from "@thebookingkit/server";
const matching = matchWebhookSubscriptions(
allSubscriptions,
"BOOKING_CONFIRMED",
{ eventTypeId: booking.eventTypeId },
);

Validates a subscription before saving it. Throws WebhookValidationError if the URL or triggers are invalid.

TypeDescription
WebhookTriggerUnion of all 21 trigger strings (e.g. "BOOKING_CONFIRMED", "SLOT_RELEASED")
WebhookPayloadStandard payload body
WebhookEnvelopeFull envelope with triggerEvent, createdAt, and payload
WebhookSubscriptionSubscription record with URL, triggers, secret, and optional scope
WebhookDeliveryResultDelivery attempt result with response code and success flag
WebhookRetryConfigRetry config: maxRetries and backoffSeconds array
WebhookVerificationResult{ valid: boolean; reason?: string }

Constants

ConstantValue
SIGNATURE_HEADER"X-BookingKit-Signature"
TIMESTAMP_HEADER"X-BookingKit-Timestamp"
DEFAULT_TOLERANCE_SECONDS300
DEFAULT_RETRY_CONFIG{ maxRetries: 3, backoffSeconds: [10, 60, 300] }
WEBHOOK_TRIGGERSArray of all 21 valid trigger strings

Generates a new API key. The full key is returned only once and must be shown to the user immediately. Only the hash is stored in the database.

import { generateApiKey } from "@thebookingkit/server";
const { key, prefix, hash } = generateApiKey("sk_live_");
// key: "sk_live_a3f2..." (show once, never store)
// prefix: "sk_live_a3f2..." (display prefix for UI)
// hash: "e9b1..." (store in DB as keyHash)

Parameters

ParameterTypeDefaultDescription
prefixstring"sk_live_"Key prefix (e.g. "sk_test_")

Hashes an API key with HMAC-SHA256 using the THEBOOKINGKIT_API_KEY_SECRET environment variable. Throws if the secret is not set.

import { hashApiKey } from "@thebookingkit/server";
const hash = hashApiKey(incomingKey);

Verifies an incoming API key against a stored hash using constant-time comparison.

import { verifyApiKey } from "@thebookingkit/server";
const valid = verifyApiKey(incomingKey, storedRecord.keyHash);

Checks whether an API key’s scopes include the required scope. The "admin" scope grants all permissions.

import { hasScope } from "@thebookingkit/server";
if (!hasScope(apiKey.scopes, "write:bookings")) {
return Response.json({ error: "Insufficient scope" }, { status: 403 });
}
import { isKeyExpired } from "@thebookingkit/server";
if (isKeyExpired(apiKey.expiresAt)) {
return Response.json({ error: "API key expired" }, { status: 401 });
}

API key scopes

"read:bookings" | "write:bookings" | "read:availability" | "write:availability" | "read:event-types" | "write:event-types" | "read:webhooks" | "write:webhooks" | "read:analytics" | "admin"

import { checkRateLimit } from "@thebookingkit/server";
const { result, newState } = checkRateLimit(
storedRateLimitState, // null for first request
apiKey.rateLimit, // requests per minute
);
if (!result.allowed) {
return new Response("Too Many Requests", {
status: 429,
headers: {
"X-RateLimit-Limit": String(result.limit),
"X-RateLimit-Remaining": String(result.remaining),
"X-RateLimit-Reset": String(result.resetMs),
},
});
}
// Persist newState to your rate limit store

Uses a fixed 1-minute sliding window. Returns remaining, resetMs (timestamp in ms), and limit.


Wraps a database operation in retry logic for SERIALIZABLE transaction isolation. Handles two Postgres error codes:

  • 40001 (serialization_failure): retries up to maxRetries times with jittered exponential backoff.
  • 23P01 (exclusion_violation): immediately throws BookingConflictError — the slot is taken, retry would not help.
import { withSerializableRetry } from "@thebookingkit/server";
const booking = await withSerializableRetry(
() => db.transaction(async (tx) => {
// Check availability
const existing = await tx.select()...
if (!isSlotAvailable(...)) throw new BookingConflictError();
// Insert with EXCLUDE constraint protection
const [row] = await tx.insert(bookings).values({...}).returning();
return row;
}),
{ maxRetries: 3, baseDelayMs: 50 },
);

Parameters

ParameterTypeDefaultDescription
fn() => Promise<T>Async operation to wrap
options.maxRetriesnumber3Maximum retry attempts on serialization failure
options.baseDelayMsnumber50Base delay for exponential backoff in milliseconds

Backoff formula: baseDelayMs * 2^attempt + jitter where jitter is a random value in [0, baseDelayMs).

Throws SerializationRetryExhaustedError when all retries are exhausted.


Standard response envelope utilities for Next.js API routes.

import {
createErrorResponse,
createSuccessResponse,
createPaginatedResponse,
API_ERROR_CODES,
} from "@thebookingkit/server";
// Error response
return Response.json(
createErrorResponse(API_ERROR_CODES.NOT_FOUND, "Booking not found"),
{ status: 404 },
);
// Success response
return Response.json(createSuccessResponse(booking));
// Paginated list
return Response.json(
createPaginatedResponse(items, nextCursor, total),
);

API_ERROR_CODES values

NOT_FOUND | UNAUTHORIZED | FORBIDDEN | VALIDATION_ERROR | CONFLICT | RATE_LIMITED | INTERNAL_ERROR | PAYMENT_REQUIRED

import { encodeCursor, decodeCursor } from "@thebookingkit/server";
const cursor = encodeCursor({ id: lastItem.id, createdAt: lastItem.createdAt });
const decoded = decodeCursor(cursor); // => { id: "...", createdAt: "..." } | null
import { validateSlotQueryParams, parseSortParam } from "@thebookingkit/server";
const validation = validateSlotQueryParams({
providerId: params.get("providerId") ?? undefined,
start: params.get("start") ?? undefined,
end: params.get("end") ?? undefined,
timezone: params.get("timezone") ?? undefined,
});
if (!validation.valid) {
return Response.json(
createErrorResponse("VALIDATION_ERROR", "Invalid parameters", {
errors: validation.errors,
}),
{ status: 400 },
);
}

validateSlotQueryParams checks that providerId or teamId is present, validates UUID format, validates date strings, and enforces a 90-day maximum range.

parseSortParam parses sort strings like "-createdAt" (descending) or "startsAt" (ascending). Returns null for unknown fields.


import {
sendConfirmationEmail,
sendReminderEmail,
sendCancellationEmail,
sendRescheduleEmail,
scheduleAutoReject,
syncBookingToCalendar,
deleteBookingFromCalendar,
formatDateTimeForEmail,
formatDurationForEmail,
} from "@thebookingkit/server";

These functions accept a NotificationBookingData payload and delegate to your configured EmailAdapter and CalendarAdapter.

Pre-built HTML and plain-text templates with {{variable}} interpolation.

import {
interpolateTemplate,
CONFIRMATION_EMAIL_HTML,
CONFIRMATION_EMAIL_TEXT,
REMINDER_EMAIL_HTML,
CANCELLATION_EMAIL_HTML,
RESCHEDULE_EMAIL_HTML,
type EmailTemplateVars,
} from "@thebookingkit/server";
const html = interpolateTemplate(CONFIRMATION_EMAIL_HTML, {
customerName: booking.customerName,
eventTitle: eventType.title,
startTime: formatDateTimeForEmail(booking.startsAt, booking.timezone),
// ...
});
import type { EmailAdapter, SendEmailOptions, EmailResult } from "@thebookingkit/server";
class ResendEmailAdapter implements EmailAdapter {
async sendEmail(options: SendEmailOptions): Promise<EmailResult> {
// delegate to Resend, SendGrid, AWS SES, or Postmark
}
}

Evaluates a set of workflow conditions against a context object. Returns true only if all conditions pass.

import { evaluateConditions } from "@thebookingkit/server";
const matches = evaluateConditions(workflow.conditions, {
bookingStatus: "confirmed",
eventTypeId: booking.eventTypeId,
});

Finds all active workflows that match a trigger event and whose conditions evaluate to true for the given context.

import { matchWorkflows } from "@thebookingkit/server";
const triggered = matchWorkflows(allWorkflows, "booking_confirmed", context);

Validates a workflow definition. Throws WorkflowValidationError if the structure is invalid.

Replaces {{variable}} placeholders in workflow action templates with values from the booking context.

TypeDescription
WorkflowTrigger"booking_created" | "booking_confirmed" | "booking_cancelled" | "booking_rescheduled" | "before_event" | "after_event"
WorkflowActionType"send_email" | "send_sms" | "webhook" | "update_status" | "add_to_calendar"
WorkflowCondition{ field, operator, value }
ConditionOperator"eq" | "neq" | "contains" | "gt" | "lt"
WorkflowDefinitionFull workflow with trigger, conditions, and actions
WorkflowContextBooking data passed to condition evaluation

Resolves the effective configuration for a booking request by cascading settings from global defaults through organization, provider, and event type levels. More specific settings override less specific ones.

import { resolveEffectiveSettings, GLOBAL_DEFAULTS } from "@thebookingkit/server";
const settings = resolveEffectiveSettings(
GLOBAL_DEFAULTS,
orgSettings,
providerSettings,
eventTypeSettings,
);
// => ResolvedSettings with timezone, currency, bufferMinutes, branding, bookingLimits

Throws TenantAuthorizationError if the member does not hold the required permission within the organization.

import { assertOrgPermission } from "@thebookingkit/server";
assertOrgPermission(member, "manage:members");

getRolePermissions() and roleHasPermission()

Section titled “getRolePermissions() and roleHasPermission()”
import { getRolePermissions, roleHasPermission } from "@thebookingkit/server";
const perms = getRolePermissions("admin");
const can = roleHasPermission("member", "view:own-bookings"); // true

buildOrgBookingUrl() and parseOrgBookingPath()

Section titled “buildOrgBookingUrl() and parseOrgBookingPath()”

Utilities for generating and parsing organization-scoped booking URLs.

import { buildOrgBookingUrl, parseOrgBookingPath } from "@thebookingkit/server";
const url = buildOrgBookingUrl("acme-corp", "consultation");
// => "/org/acme-corp/consultation"
const parsed = parseOrgBookingPath("/org/acme-corp/consultation");
// => { orgSlug: "acme-corp", eventSlug: "consultation" }
TypeFields
OrgRole"owner" | "admin" | "member"
OrgMemberuserId, organizationId, role
OrgSettingsdefaultTimezone, defaultCurrency, branding, defaultBufferMinutes, defaultBookingLimits
ProviderSettingstimezone, currency, branding, bufferMinutes, bookingLimits
EventTypeSettingstimezone, currency, bufferBefore, bufferAfter, bookingLimits
ResolvedSettingsFully merged timezone, currency, bufferMinutes, branding, bookingLimits
OrgPermission"manage:members" | "manage:teams" | "manage:event-types" | "view:all-bookings" | "view:own-bookings" | "manage:own-availability"

Constants

import { GLOBAL_DEFAULTS } from "@thebookingkit/server";
// => { timezone: "UTC", currency: "usd", bufferMinutes: 0 }

All external dependencies are abstracted behind these interfaces. Pass implementations to the functions that accept adapters.

import type {
CalendarAdapter,
CalendarEventOptions,
CalendarEventResult,
JobAdapter,
StorageAdapter,
SmsAdapter,
SendSmsOptions,
SmsResult,
PaymentAdapter,
CreatePaymentIntentOptions,
CreatePaymentIntentResult,
CaptureResult,
RefundResult,
} from "@thebookingkit/server";
InterfaceDefault implementationAlternatives
AuthAdapterNextAuth.jsSupabase Auth, Clerk, Lucia
EmailAdapterResendSendGrid, AWS SES, Postmark
CalendarAdapterGoogle Calendar (OAuth)Outlook, Apple Calendar
JobAdapterInngestTrigger.dev, BullMQ, Vercel Cron
StorageAdapterEnv var encryption keyKMS, Vault
SmsAdapterTwilioVonage, AWS SNS
PaymentAdapterStripeSquare, Adyen
import { generateICSAttachment, JOB_NAMES } from "@thebookingkit/server";
const icsContent = generateICSAttachment({
bookingId: booking.id,
title: eventType.title,
startsAt: booking.startsAt,
endsAt: booking.endsAt,
timezone: booking.timezone,
organizerName: provider.name,
organizerEmail: provider.email,
});

JOB_NAMES is a constant object with all background job name strings used by the JobAdapter integration.


import {
BookingConflictError,
SerializationRetryExhaustedError,
UnauthorizedError,
ForbiddenError,
ResourceUnavailableError,
} from "@thebookingkit/server";

These are re-exported from @thebookingkit/core for convenience so server code does not need to depend on both packages to throw and catch the same error types.

Error classHTTP statusWhen thrown
BookingConflictError409Slot already booked (exclusion constraint or optimistic check)
SerializationRetryExhaustedError500SQLSTATE 40001 exhausted all retry attempts
UnauthorizedError401No valid session or token
ForbiddenError403Authenticated but insufficient permissions
ResourceUnavailableError409Requested resource (room, equipment) is unavailable