Skip to content

Adapters

The Booking Kit uses the adapter pattern to abstract external dependencies. Each adapter is a TypeScript interface — implement it for your preferred provider.

interface AuthAdapter {
getSession(request: Request): Promise<AuthSession | null>;
getUser(userId: string): Promise<AuthUser | null>;
}

Default: NextAuth.js 5.x Alternatives: Supabase Auth, Clerk, Lucia, custom JWT

interface EmailAdapter {
sendEmail(options: SendEmailOptions): Promise<EmailResult>;
}
interface SendEmailOptions {
to: string;
subject: string;
html: string;
text?: string;
from?: string;
replyTo?: string;
attachments?: EmailAttachment[];
}

Default: Resend Alternatives: SendGrid, AWS SES, Postmark, Nodemailer

interface CalendarAdapter {
createEvent(options: CalendarEventOptions): Promise<CalendarEventResult>;
updateEvent(eventId: string, options: CalendarEventOptions): Promise<CalendarEventResult>;
deleteEvent(eventId: string): Promise<void>;
getConflicts(start: Date, end: Date): Promise<CalendarConflict[]>;
}

Default: Google Calendar (OAuth) Alternatives: Microsoft Outlook, CalDAV

interface JobAdapter {
enqueue(jobName: string, payload: unknown, options?: { delay?: number }): Promise<void>;
scheduleAt(jobName: string, payload: unknown, runAt: Date): Promise<void>;
}

Default: Inngest 3.x Alternatives: Trigger.dev, BullMQ, Vercel Cron, pg-boss

interface PaymentAdapter {
createPaymentIntent(options: CreatePaymentIntentOptions): Promise<CreatePaymentIntentResult>;
createSetupIntent(options: CreateSetupIntentOptions): Promise<CreateSetupIntentResult>;
capturePayment(paymentIntentId: string): Promise<CaptureResult>;
refundPayment(paymentIntentId: string, amountCents?: number): Promise<RefundResult>;
}

Default: Stripe Alternatives: Implement for your payment provider

interface StorageAdapter {
encrypt(plaintext: string): Promise<string>;
decrypt(ciphertext: string): Promise<string>;
}

Default: Env var encryption key (AES-256) Alternatives: HashiCorp Vault, AWS KMS, Azure Key Vault

interface SmsAdapter {
sendSms(options: SendSmsOptions): Promise<SmsResult>;
}

Default: Not implemented (optional) Alternatives: Twilio, MessageBird, Vonage

While The Booking Kit is built for PostgreSQL, it provides specialized adapters for other database environments.

Cloudflare D1 / SQLite (@thebookingkit/d1)

Section titled “Cloudflare D1 / SQLite (@thebookingkit/d1)”

Cloudflare D1 requires special handling for date storage and concurrency. This package provides:

  • Canonical Date Encoding: SQLite has no native timestamp type. D1DateCodec ensures all dates are stored as sortable UTC-Z strings.
  • Advisory Locking: Since SQLite lacks EXCLUDE constraints, D1BookingLock provides application-level mutexes to prevent double-booking.
  • Schedule Adapters: Utilities to intersect personal and location schedules into RRULE windows.
import { D1DateCodec, D1BookingLock } from "@thebookingkit/d1";
// Ensure a slot is available and book it atomically
const lock = new D1BookingLock(db);
await lock.withLock(`provider:${id}`, async () => {
// Read-check-write logic here
});
import sgMail from "@sendgrid/mail";
import type { EmailAdapter } from "@thebookingkit/core";
export const sendGridEmailAdapter: EmailAdapter = {
async sendEmail(options) {
sgMail.setApiKey(process.env.SENDGRID_API_KEY!);
try {
const [response] = await sgMail.send({
to: options.to,
from: options.from || "bookings@example.com",
subject: options.subject,
html: options.html,
text: options.text,
replyTo: options.replyTo,
});
return {
messageId: response.headers["x-message-id"] as string,
status: "sent" as const,
};
} catch (error) {
console.error("SendGrid error:", error);
return {
status: "failed" as const,
error: error instanceof Error ? error.message : "Unknown error",
};
}
},
};
import { createClient } from "@supabase/supabase-js";
import type { AuthAdapter, AuthSession, AuthUser } from "@thebookingkit/core";
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
export const supabaseAuthAdapter: AuthAdapter = {
async getSession(request: Request): Promise<AuthSession | null> {
const token = request.headers.get("authorization")?.split("Bearer ")[1];
if (!token) return null;
const { data, error } = await supabase.auth.getUser(token);
if (error || !data.user) return null;
return {
userId: data.user.id,
email: data.user.email!,
role: "user",
};
},
async getUser(userId: string): Promise<AuthUser | null> {
const { data, error } = await supabase.auth.admin.getUserById(userId);
if (error || !data.user) return null;
return {
id: data.user.id,
email: data.user.email!,
name: data.user.user_metadata?.name,
};
},
};
import { TriggerClient, eventTrigger } from "@trigger.dev/sdk/v3";
import type { JobAdapter } from "@thebookingkit/core";
const client = new TriggerClient({ id: "booking-kit" });
export const triggerJobAdapter: JobAdapter = {
async enqueue(jobName: string, payload: unknown, options?: { delay?: number }) {
await client.sendEvent({
name: jobName,
payload,
timestamp: new Date(Date.now() + (options?.delay || 0)),
});
},
async scheduleAt(jobName: string, payload: unknown, runAt: Date) {
await client.sendEvent({
name: jobName,
payload,
timestamp: runAt,
});
},
};

Pass adapters to the functions that need them:

import { sendConfirmationEmail } from "@thebookingkit/server";
import { sendGridEmailAdapter } from "@/adapters/email";
// Email will be sent via SendGrid
await sendConfirmationEmail(
bookingPayload,
sendGridEmailAdapter
);
import type { EmailAdapter } from "@thebookingkit/core";
// Mock adapter for testing
const mockEmailAdapter: EmailAdapter = {
async sendEmail(options) {
console.log(`[MOCK] Sending email to ${options.to}: ${options.subject}`);
return {
messageId: "mock-" + Date.now(),
status: "sent" as const,
};
},
};
// Use in tests
describe("Booking confirmation", () => {
it("sends confirmation email", async () => {
await sendConfirmationEmail(bookingData, mockEmailAdapter);
// Email was logged to console for verification
});
});

Adapters are injected at the call site, making them easy to test and swap between providers.