Appointment Booking
Overview
An add-on app for booking appointments and queue slots through LIFF. The customer picks a location, then a service, a staff member and a date and time, and receives a booking with reminders sent ahead of the appointment.
Architecturally, the important point is that all of its tables live in a separate PostgreSQL
schema named appointment, not in public, and none of them exist in Prisma. The app
maintains its own numbered SQL files that are applied with psql — the same approach used by the
bulletin and loyalty apps.
Core Data Structure
appointment.location — branches and venues
- Basic details:
name,description,address,phone working_hoursas JSONB holds the daily opening hours;blocked_datesholds closurestimezone(defaults toAsia/Bangkok),sort_order,statusand soft deletedeleted_date- Scoped by
line_oa_idandorganization_id
appointment.service — bookable services
location_idis a foreign key withON DELETE CASCADEduration_minutes(defaults to 30),max_bookings_per_slot(defaults to 1) andrequires_staffare the three values that drive slot availability- Pricing:
priceas DECIMAL(10,2) andcurrency(defaults to THB)
appointment.staff — service providers
location_idforeign key, plusname,titleandavatar_urlservice_idsas JSONB lists the services this person can perform, stored as an array rather than in a separate join table
appointment.journey — the booking flow configuration
One journey equals one booking link.
tokenVARCHAR(64) is unique and serves as the key in the LIFF URLauto_confirm(defaults to true) andstepsas JSONB define the sequence the customer seesreminder_hours_before(defaults to 24) andno_show_grace_minutes(defaults to 30)
appointment.booking — the booking itself
- Foreign keys:
journey_id,location_id,service_idandstaff_id(nullable) user_idVARCHAR(255) is the LINE userId of the person booking- Timing:
booking_dateas DATE, withstart_timeandend_timeas TIME - State:
status(defaults topending),notes,form_dataas JSONB andcancel_reason, plus a timestamp for each terminal state —confirmed_date,cancelled_date,completed_date reminder_sentcomes with the partial index(status, reminder_sent, booking_date) WHERE reminder_sent = false, which lets the cron find un-reminded bookings quickly- The
(location_id, booking_date, status)index backs the CMS calendar view
Booking event triggers
trg_booking_event_insert and trg_booking_event_update call
appointment.notify_booking_event(), which emits pg_notify on the booking_event channel and
also writes to event_outbox on a best-effort basis, catching undefined_table and skipping if
the outbox is not present.
Events fire when a booking is created (booking_created) and only when its status changes
(booking_status_changed) — not on every row update.
Related Files
apps/appointment/migrations/001_create_schema.sql— theCREATE SCHEMA appointmentstatementapps/appointment/migrations/002_create_tables.sql— all five core tablesapps/appointment/migrations/003_add_reminder_columns.sql— the reminder columnsapps/appointment/migrations/004_add_booking_event_trigger.sql—pg_notifyand the outboxschema-dumps/2026-07-24/schema.sql:26(CREATE SCHEMA appointment) and the:582–:807range
Connections to Other Services
- client-api-go serves the customer-facing LIFF. It computes available slots from
working_hourscombined withblocked_dates,duration_minutesandmax_bookings_per_slotbefore creating the booking. - cms-api-go manages locations, services, staff, journeys and the booking calendar.
- worker-go listens on
booking_eventto send LINE notifications and to act as a trigger-rule source, and runs the cron that sends reminders on schedule. - The app is enabled per channel through
line_oa_app.app_idin LINE OA Channels, and its events flow into pg_notify & Event Outbox.