Booking Source Code: Build a Full System with React Native
Get the booking source code to build a complete system. Learn to use AppLighter, Expo, React Native, Supabase, & Hono/TypeScript in 2026.

You're probably in one of two spots right now. Either you've promised a booking app in a sprint planning meeting and then remembered what “booking” means in production, or you've already built the first version and discovered that availability, state, payments, and attribution don't forgive shortcuts.
A booking system looks simple until the first overlap bug, the first abandoned checkout that never releases inventory, or the first support ticket asking why the reservation source code disappeared after an edit. That's why I don't like starting from a blank repo for this category. Booking software has too many moving parts, and most of the expensive mistakes happen in plumbing, not in UI polish.
There's a reason this domain has such a long engineering history. The modern airline booking source-code ecosystem goes back to the first computer reservation systems in the 1960s, when American Airlines and IBM developed SABRE, completed in 1964, and it could process over 7,000 bookings per hour with a nearly zero error rate, according to AltexSoft's history of CRSs and GDSs. The lesson still holds. Booking software wins or loses on transaction design.
If you want a practical view of how specialized operators handle this in adjacent industries, this breakdown of booking and quoting software for movers is worth reading. Different workflow, same reality: once scheduling, pricing, and operations meet, weak systems unravel fast.
Table of Contents
- Laying the Foundation for Your Booking System
- Understanding the AppLighter Stack
- Designing Your Booking Database Schema
- Building the Booking API with Hono and TypeScript
- Implementing the UI with Expo and React Native
- From Code to Live Production
Laying the Foundation for Your Booking System
Starting from zero is where most booking projects lose weeks. Teams burn time on auth wiring, navigation, API shape debates, and database bootstrapping before they even define what a reservation is. That's avoidable if you begin with a stack that already agrees with itself.
A minimalist home office workspace featuring a computer monitor, a rolled blueprint, and a potted plant.
The fastest route is to treat booking source code as a full system problem, not a screen-building exercise. You need a mobile client, a dependable API, persistent state, reservation lifecycle rules, and enough structure that another developer can pick up the codebase without reverse-engineering your intent. If you're building with React Native, a pre-wired foundation changes the project from “assemble infrastructure” to “implement business rules.”
A good starter kit also forces useful discipline. It nudges you toward typed routes, shared validation, and a real persistence layer instead of the “just keep it in local state for now” trap that inadvertently becomes production architecture.
Practical rule: if a booking app can't survive two users tapping the same slot at once, you don't have a booking system yet.
For teams using React Native, AppLighter is one way to skip the repetitive setup work because it comes preconfigured around Expo, an edge-ready API layer, and a database workflow that fits mobile delivery. If you want the React Native side of that setup in more depth, the guide to building an app with React Native is a useful companion read.
Understanding the AppLighter Stack
A booking app gets easier when the stack is opinionated in the right places. You want fewer choices at the infrastructure layer and more freedom in the product layer. That's why this stack makes sense for a booking source code implementation: Expo and React Native on the client, Hono with TypeScript in the API, and a Postgres-backed data layer managed through schema-first tooling.
A diagram illustrating the AppLighter stack, featuring frontend, backend API, and data storage components for mobile development.
Why this combination works
Expo and React Native solve the obvious part. You get one codebase targeting iOS and Android, and you don't spend the first phase of the project negotiating native setup. For booking flows, that matters because most of your hard work isn't visual. It's request timing, retries, state transitions, and making sure the confirmation screen never lies.
Hono is a strong API fit because it stays small and typed. Booking endpoints benefit from that. You can keep routes explicit, validation close to handlers, and business logic easy to isolate into services. That reduces the common drift where frontend assumptions and backend behavior slowly diverge.
A Postgres-backed data layer is the essential component. Reservations, availability windows, payment status, source codes, expirations, and audit history all need persistence that survives process restarts and scales beyond a single runtime.
Here's the practical breakdown:
| Layer | What it handles | Why it matters for booking |
|---|---|---|
| Frontend | Slot browsing, hold review, confirmation UI | Keeps user state understandable during a multi-step flow |
| API | Availability logic, hold creation, booking finalization | Centralizes rules that must be consistent under concurrency |
| Data | Services, schedules, reservations, status history | Preserves truth when users retry, cancel, or amend bookings |
What you skip by not starting from scratch
Configuration debt is frequently underestimated. In greenfield booking projects, the first avoidable delays usually come from:
- Auth drift: mobile screens assume a user context the API hasn't standardized.
- Type gaps: one side sends
serviceId, the other expectsservice_id. - Environment confusion: local, preview, and production point at mismatched backends.
- State duplication: optimistic UI state pretends a booking exists before the backend agrees.
Build the business rules first. Polish the booking card later.
An integrated starter helps. It narrows the places bugs can hide. Instead of assembling libraries that may or may not cooperate, you work inside an already connected system and spend your energy on reservation behavior, source-code mapping, and shipping.
Designing Your Booking Database Schema
The database is where a booking source code project becomes real. If the schema is vague, the rest of the app gets vague with it. Developers then start patching logic into controllers and components, and availability becomes a rumor instead of a fact.
A lot of booking implementations fail at the state-management layer, especially when teams mix in-memory globals, ad hoc ID generation, and weak validation. A more durable pattern is an API-backed persistence layer with explicit reservation history endpoints, as noted in this booking source code implementation discussion. That's the baseline. If state matters to customers, it belongs in the database.
For schema work in this kind of stack, a Supabase schema generator for mobile apps can speed up the boring part without changing the important part, which is still model design.
Model the booking lifecycle first
Don't start with “what fields should Booking have?” Start with “what states can a reservation enter, and what transitions are legal?” That one shift prevents a lot of future pain.
For a production flow, I usually want these concepts:
- Service for the thing being booked
- AvailabilityWindow for when the service can be reserved
- Reservation for the hold or confirmed booking
- ReservationEvent for audit history
- SourceCode or a normalized channel field for attribution and reporting
The key design choice is that a reservation can exist before it's finalized. If you only store confirmed bookings, you lose the hold state, and then you end up faking it in memory or in the client.
Your schema should answer support questions without requiring log archaeology.
A Prisma schema you can actually ship
Here's a compact Prisma example that supports service browsing, time-window inventory, expiring holds, confirmation, and source-code capture.
enum ReservationStatus {
HOLD
CONFIRMED
CANCELLED
EXPIRED
}
model Service {
id String @id @default(cuid())
name String
slug String @unique
description String?
isActive Boolean @default(true)
availabilities AvailabilityWindow[]
reservations Reservation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model AvailabilityWindow {
id String @id @default(cuid())
serviceId String
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
startsAt DateTime
endsAt DateTime
capacity Int
isBookable Boolean @default(true)
reservations Reservation[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([serviceId, startsAt])
}
model Reservation {
id String @id @default(cuid())
reservationId String @unique
serviceId String
availabilityId String
customerId String?
status ReservationStatus @default(HOLD)
sourceCode String?
guestName String
guestEmail String
holdExpiresAt DateTime?
confirmedAt DateTime?
cancelledAt DateTime?
service Service @relation(fields: [serviceId], references: [id], onDelete: Restrict)
availability AvailabilityWindow @relation(fields: [availabilityId], references: [id], onDelete: Restrict)
events ReservationEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([availabilityId, status])
@@index([guestEmail])
@@index([sourceCode])
}
model ReservationEvent {
id String @id @default(cuid())
reservationId String
type String
payload Json?
createdAt DateTime @default(now())
reservation Reservation @relation(fields: [reservationId], references: [id], onDelete: Cascade)
@@index([reservationId, createdAt])
}
A few choices here matter more than they look.
reservationIdis separate from the primary key. That lets you expose a stable public identifier without leaking internal assumptions.holdExpiresAtis first-class data. Expiry isn't a side note. It's part of inventory control.ReservationEventexists from day one. If reservations are edited later, auditability matters.sourceCodeis on the reservation itself. You can normalize further, but don't leave attribution stranded in client analytics alone.
One more opinionated call. Avoid encoding too much availability logic directly into static rows if your scheduling rules are complex. For recurring schedules, generated windows plus reservations tend to be easier to reason about than trying to calculate everything on the fly during checkout.
Building the Booking API with Hono and TypeScript
This is the part where most booking demos become unrealistic. They create a booking in one request, skip the hold phase, and pretend concurrency won't happen. In production, that's where support tickets come from.
The more reliable pattern is a two-phase reservation flow: create a reservation order or hold first, then finalize it after payment or explicit confirmation. ByteByteGo's reservation-system design notes that this reservation order and unique reservation_id are the core control point for preventing double-booking under concurrency in its hotel reservation system design discussion.
A five-step diagram illustrating the development flow for a booking API, from database definition to edge deployment.
The endpoint split that prevents checkout pain
If your API only has POST /bookings, you've already compressed too many concerns into one call. I prefer this split:
GET /servicesGET /services/:slug/availability?date=...POST /reservationsPOST /reservations/:reservationId/confirmDELETE /reservations/:reservationIdGET /reservations/:reservationId
That shape maps to user intent. Browse, choose, hold, confirm, or release. It also makes failures easier to handle. A payment timeout shouldn't leave you guessing whether inventory is still blocked.
A short architecture walkthrough helps before the code:
Hono route examples
The first route lists bookable services.
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
import { prisma } from '../lib/prisma'
const app = new Hono()
app.get('/services', async (c) => {
const services = await prisma.service.findMany({
where: { isActive: true },
orderBy: { name: 'asc' },
select: {
id: true,
name: true,
slug: true,
description: true,
},
})
return c.json({ services })
})
Then availability for a given service and day.
const availabilityQuerySchema = z.object({
date: z.string().min(1),
})
app.get(
'/services/:slug/availability',
zValidator('query', availabilityQuerySchema),
async (c) => {
const { slug } = c.req.param()
const { date } = c.req.valid('query')
const startOfDay = new Date(`${date}T00:00:00.000Z`)
const endOfDay = new Date(`${date}T23:59:59.999Z`)
const service = await prisma.service.findUnique({
where: { slug },
select: { id: true, name: true },
})
if (!service) {
return c.json({ error: 'Service not found' }, 404)
}
const windows = await prisma.availabilityWindow.findMany({
where: {
serviceId: service.id,
isBookable: true,
startsAt: { gte: startOfDay, lte: endOfDay },
},
orderBy: { startsAt: 'asc' },
include: {
reservations: {
where: {
status: { in: ['HOLD', 'CONFIRMED'] },
},
select: { id: true, status: true, holdExpiresAt: true },
},
},
})
const availability = windows.map((window) => {
const activeReservations = window.reservations.filter((r) => {
if (r.status === 'CONFIRMED') return true
if (r.status === 'HOLD' && r.holdExpiresAt) {
return r.holdExpiresAt > new Date()
}
return false
})
return {
id: window.id,
startsAt: window.startsAt,
endsAt: window.endsAt,
capacity: window.capacity,
remaining: Math.max(window.capacity - activeReservations.length, 0),
}
})
return c.json({ service, availability })
}
)
The important route is POST /reservations. This creates the hold.
const createReservationSchema = z.object({
serviceId: z.string().min(1),
availabilityId: z.string().min(1),
guestName: z.string().min(1),
guestEmail: z.string().email(),
sourceCode: z.string().optional(),
})
app.post(
'/reservations',
zValidator('json', createReservationSchema),
async (c) => {
const body = c.req.valid('json')
const now = new Date()
const holdExpiresAt = new Date(now.getTime() + 10 * 60 * 1000)
const result = await prisma.$transaction(async (tx) => {
const window = await tx.availabilityWindow.findUnique({
where: { id: body.availabilityId },
include: {
reservations: {
where: {
status: { in: ['HOLD', 'CONFIRMED'] },
},
},
},
})
if (!window || !window.isBookable || window.serviceId !== body.serviceId) {
throw new Error('Invalid availability selection')
}
const activeCount = window.reservations.filter((r) => {
if (r.status === 'CONFIRMED') return true
if (r.status === 'HOLD' && r.holdExpiresAt) {
return r.holdExpiresAt > now
}
return false
}).length
if (activeCount >= window.capacity) {
throw new Error('Selected slot is no longer available')
}
const reservation = await tx.reservation.create({
data: {
reservationId: crypto.randomUUID(),
serviceId: body.serviceId,
availabilityId: body.availabilityId,
guestName: body.guestName,
guestEmail: body.guestEmail,
sourceCode: body.sourceCode,
status: 'HOLD',
holdExpiresAt,
events: {
create: {
type: 'HOLD_CREATED',
payload: body,
},
},
},
})
return reservation
})
return c.json({
reservationId: result.reservationId,
status: result.status,
holdExpiresAt: result.holdExpiresAt,
}, 201)
}
)
And then confirmation:
app.post('/reservations/:reservationId/confirm', async (c) => {
const { reservationId } = c.req.param()
const reservation = await prisma.reservation.findUnique({
where: { reservationId },
})
if (!reservation) {
return c.json({ error: 'Reservation not found' }, 404)
}
if (reservation.status !== 'HOLD') {
return c.json({ error: 'Reservation is not in HOLD state' }, 409)
}
if (!reservation.holdExpiresAt || reservation.holdExpiresAt <= new Date()) {
await prisma.reservation.update({
where: { reservationId },
data: { status: 'EXPIRED' },
})
return c.json({ error: 'Reservation hold expired' }, 409)
}
const updated = await prisma.reservation.update({
where: { reservationId },
data: {
status: 'CONFIRMED',
confirmedAt: new Date(),
events: {
create: {
type: 'RESERVATION_CONFIRMED',
},
},
},
})
return c.json({
reservationId: updated.reservationId,
status: updated.status,
confirmedAt: updated.confirmedAt,
})
})
What to validate before touching the database
Teams often go too light. Input validation should check more than types.
- Service and window alignment: the selected availability must belong to the service sent by the client.
- Current bookability: disabled windows must fail cleanly.
- Time validity: don't allow holds on windows that have already passed.
- Source-code hygiene: accept only known values or normalize them before storage.
- Idempotency behavior: if the client retries, know whether you're creating a new hold or returning an existing state.
Booking bugs rarely start with SQL. They start with a missing state rule.
Also add cleanup. Expired holds should be reclaimed by a scheduled process. If you skip that, your availability endpoint will slowly become dishonest.
Implementing the UI with Expo and React Native
The frontend job is simple to describe and easy to get wrong. It must show availability clearly, keep the user moving, and never imply a booking is confirmed before the backend says so.
Screenshot from https://www.applighter.com
If you're choosing component patterns before building the screens, this roundup of React Native UI libraries helps narrow the styling layer without overcomplicating the app shell.
The three screens that matter
A booking flow doesn't need many screens. It needs honest screens.
The first is ServiceListScreen. Keep it blunt. Show the bookable services, short descriptions, and a CTA that moves into date selection. Don't put availability calculations here unless they're cheap and reliable.
export function ServiceListScreen({ navigation }) {
const { data, isLoading, error } = useServices()
if (isLoading) return <LoadingState />
if (error) return <ErrorState message="Couldn't load services" />
return (
<FlatList
data={data.services}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Pressable onPress={() => navigation.navigate('Calendar', { slug: item.slug })}>
<Text>{item.name}</Text>
{item.description ? <Text>{item.description}</Text> : null}
<Text>Book now</Text>
</Pressable>
)}
/>
)
}
The second is CalendarScreen. This one does the heavy lifting in the user journey. The user chooses a date, sees slot inventory, taps one, and creates a hold.
export function CalendarScreen({ route, navigation }) {
const { slug } = route.params
const [date, setDate] = useState(getTodayISO())
const [selectedSlot, setSelectedSlot] = useState<string | null>(null)
const availabilityQuery = useAvailability(slug, date)
const createReservation = useCreateReservation()
const onContinue = async () => {
if (!availabilityQuery.data || !selectedSlot) return
const service = availabilityQuery.data.service
const result = await createReservation.mutateAsync({
serviceId: service.id,
availabilityId: selectedSlot,
guestName: 'Taylor Guest',
guestEmail: 'taylor@example.com',
sourceCode: 'DIRECT_MOBILE',
})
navigation.navigate('Confirmation', {
reservationId: result.reservationId,
})
}
return (
<View>
<DatePicker value={date} onChange={setDate} />
<SlotList
slots={availabilityQuery.data?.availability ?? []}
selectedSlot={selectedSlot}
onSelect={setSelectedSlot}
/>
<Button title="Continue" onPress={onContinue} disabled={!selectedSlot} />
</View>
)
}
Wiring the client flow
The confirmation screen should reflect the actual reservation state, not optimistic local assumptions. That means it loads the reservation or receives only minimal navigation params and fetches the truth.
export function ConfirmationScreen({ route }) {
const { reservationId } = route.params
const reservationQuery = useReservation(reservationId)
const confirmMutation = useConfirmReservation()
if (reservationQuery.isLoading) return <LoadingState />
if (reservationQuery.error) return <ErrorState message="Couldn't load reservation" />
const reservation = reservationQuery.data
return (
<View>
<Text>Reservation {reservation.reservationId}</Text>
<Text>Status: {reservation.status}</Text>
{reservation.holdExpiresAt ? (
<Countdown expiresAt={reservation.holdExpiresAt} />
) : null}
<Button
title="Confirm booking"
onPress={() => confirmMutation.mutate({ reservationId })}
/>
</View>
)
}
What matters most in the UI isn't complexity. It's sequencing.
- Fetch services first
- Fetch availability by date
- Create the hold on slot selection or continue
- Show countdown and reservation details
- Confirm only after explicit user action or payment success
That sequence keeps your frontend aligned with backend truth.
UI mistakes that create backend bugs
A lot of “backend issues” are UI design mistakes. I see the same ones repeatedly.
- Premature confirmation states: the app shows “Booked” when it only has a hold.
- Stale slot rendering: the client keeps showing old availability after a failed attempt.
- No expiry feedback: users lose a hold without any visible timer or refresh path.
- Loose source-code capture: the app sends arbitrary strings instead of mapped taxonomy values.
One more opinion. Don't over-animate this flow. Booking is a task-oriented journey. Users care more about clarity than flair, especially around payment, hold time, and confirmation.
From Code to Live Production
Shipping is where a booking source code system proves whether it was designed as software or just as a demo. The code can look clean locally and still fail once reservations are edited, imported, retried, or pushed through other tools.
Support documentation in the broader booking software ecosystem shows a problem teams often ignore: missing or misconfigured source codes can break downstream processes, and the harder challenge is keeping attribution and auditability reliable when reservations are edited across environments and vendors, as reflected in BookingCenter support materials. That's not just an ops issue. It's an engineering requirement.
Test what breaks money and trust
Start with tests around lifecycle transitions. A booking system doesn't need broad shallow coverage nearly as much as it needs deep coverage around state edges.
I'd focus on:
- Reservation hold tests: creating holds, rejecting full slots, expiring stale holds.
- Confirmation tests: confirming valid holds, rejecting expired ones, handling duplicate confirmation attempts.
- Source-code integrity tests: ensuring allowed values persist through create, update, and read flows.
- Edit-path tests: verifying reservation amendments don't inadvertently drop attribution or history.
For repo hygiene and onboarding, small procedural docs help more than teams expect. If someone on the team is still fuzzy on branch and project structure basics, Tutorial AI's GitHub folder tutorial is a decent lightweight reference for keeping setup friction low.
Deployment is where integration debt shows up
A coherent stack reduces release friction, but deployment still needs deliberate rules. Put your Hono API on an edge-friendly platform that matches your environment model. Keep secrets isolated per environment. Make sure the mobile app knows which backend it should talk to in development, preview, and production.
Then test the ugly flows.
- Start a hold and let it expire.
- Retry a confirmation request.
- Edit a reservation after creation.
- Import or backfill bookings and inspect source-code consistency.
- Cancel and recreate around the same time slot.
If your logs, API responses, and admin views disagree, fix that before launch. Booking systems fail imperceptibly at first. A few bad states later, support starts doing manual repairs, and the product team stops trusting the data.
One final opinionated point. Don't treat deployment as a handoff after feature work. In booking software, deployment choices are product choices because they shape reliability, auditability, and how quickly you can fix the first real-world edge case.
If you want a faster path to shipping this kind of app, AppLighter gives you a pre-wired mobile stack with Expo, Hono, and a database workflow already connected, so you can spend your time implementing reservation logic, source-code handling, and production rules instead of rebuilding the same setup from scratch.