Docs/Concepts/Calendar & bookings

Calendar & bookings

Every Neofir org has an internal calendar — a Postgres table that holds confirmed bookings, the slots they occupy, and time-off blocks. Google Calendar is an optional one-way write target, not the source of truth.

Why internal first

Earlier designs read availability from Google Calendar at every request. That made conflict detection unreliable (Google's free-busy API has propagation lag), and it tied the agent to a calendar provider. Today the bookings table is queried for conflicts, business-hours and time-off rules are applied, and the answer is deterministic.

Bookings table

ColumnTypePurpose
idUUIDPrimary key.
organization_idUUIDFK to organizations. RLS-scoped.
conversation_idUUID nullableConversation that produced the booking.
customer_name / customer_phone / customer_emailtextCaptured from the booking flow.
service_idstext[]One or more services from the catalog.
resource_idUUID nullableMulti-resource bookings. NULL for single-resource tenants.
starts_attimestamptzSlot start.
duration_minintegerComputed from service catalog.
statusenumconfirmed / cancelled.
external_event_idtext nullableGoogle Calendar event id (if synced).
reminder_sent_attimestamptz nullableSet when the 24h reminder ran.
deleted_attimestamptz nullableSoft delete.

Conflict detection

The unique index on bookings (organization_id, COALESCE(resource_id, ZERO_UUID), starts_at) WHERE status = 'confirmed' AND deleted_at IS NULL guarantees no two confirmed bookings overlap per resource (or per org for single-resource tenants).

Conflict checks happen in the check_availability and book_slot tools: candidate slots are filtered against the bookings table, time_off blocks, and the configured business hours.

Time off

The time_off table holds intervals when slots are unavailable (vacations, holidays, ad-hoc closures). CRUD lives at /dashboard/settings → Vacanțe & închideri. Time-off blocks subtract from availability the same way confirmed bookings do.

Business hours

Configured per org under Settings → Program. Schema is per-day open/close intervals (multiple intervals supported — e.g. a lunch break).

Multi-resource

Larger orgs (multiple operators, stylists, doctors, bays) populate the resources table. Each resource has a name, optional avatar, and capacity (default 1). Bookings reference a resource_id; conflict detection scopes per resource.

Single-resource tenants leave the resources table empty. The conflict index treats them as if everything were bound to ZERO_UUID, so a single operator cannot double-book themselves.

Google Calendar sync

Sync is opt-in under Settings → Integrări → Google Calendar. After OAuth, every confirmed booking is pushed to the connected calendar; cancellations delete the event. The reverse direction (Google → Neofir) is not implemented — block time you don't want bookable in Vacanțe & închideri instead.

Reminders

A scheduled job (Railway cron, runs every 10 minutes) finds bookings within the 24h window without reminder_sent_at set and dispatches reminder messages to the channel the booking came from (email for web, DM for IG/Messenger, WhatsApp for WA). The cron sets reminder_sent_at after success.

Cancellation

Cancellation can be triggered by the customer (via the agent) or by the operator (via the dashboard). Both paths soft-delete the row, update status, and push a cancellation event to Google if the calendar is synced.

Roadmap: polymorphic interactions

Beyond appointment-style bookings, the platform's data layer is being extended to support other interaction kinds — repair tickets, consultations, quote requests, table reservations, house calls. This unlocks vertical-specific flows without per-tenant schema changes.