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.

Profile photo of SurajSuraj
9th Jun 2026
Featured image for Booking Source Code: Build a Full System with React Native

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

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.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.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:

LayerWhat it handlesWhy it matters for booking
FrontendSlot browsing, hold review, confirmation UIKeeps user state understandable during a multi-step flow
APIAvailability logic, hold creation, booking finalizationCentralizes rules that must be consistent under concurrency
DataServices, schedules, reservations, status historyPreserves 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 expects service_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.

  • reservationId is separate from the primary key. That lets you expose a stable public identifier without leaking internal assumptions.
  • holdExpiresAt is first-class data. Expiry isn't a side note. It's part of inventory control.
  • ReservationEvent exists from day one. If reservations are edited later, auditability matters.
  • sourceCode is 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.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 /services
  • GET /services/:slug/availability?date=...
  • POST /reservations
  • POST /reservations/:reservationId/confirm
  • DELETE /reservations/:reservationId
  • GET /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.comScreenshot 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.

Stay Updated on the Latest UI Templates and Features

Be the first to know about new React Native UI templates and kits, features, special promotions and exclusive offers by joining our newsletter.