Guest profiles
A unified profile per guest across reservations, vouchers, and online orders.
The guest CRM stores one profile per guest, deduplicated by email and phone. Every time a reservation, voucher, or order references that guest, the profile's history grows automatically. Hosts see this history inline when assigning tables, so a regular VIP is never treated as a stranger.
Profile schema
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| name | string | Required | — | Display name. First plus last is recommended for matching. |
| string | — | — | Used for confirmations and as a deduplication key. | |
| phone | E.164 string | — | — | Used for SMS notifications and as a secondary deduplication key. |
| language | 'de' | 'en' | — | de | Language for confirmations, reminders and the booking page. |
| allergens | string[] | — | — | Standardized list (gluten, lactose, nuts, shellfish, etc.) plus free-form entries. |
| dietary | string[] | — | — | Vegan, vegetarian, halal, kosher, pescatarian, or custom. |
| notes | string | — | — | Free-form notes visible to staff. Not surfaced to the guest. |
| tags | string[] | — | — | VIP, regular, blocked, press, birthday-month, etc. |
| visits | Visit[] | — | — | Auto-populated history of past reservations with date, party, table and server. |
| marketingConsent | boolean | — | false | Whether the guest has opted in to marketing communications. |
| customFields | Record<string, string> | — | — | Workspace-defined fields, e.g. wine club membership tier. |
Visit history
Each profile shows a chronological list of past reservations: date, party size, table, server, total spend if linked from POS, and any incident notes. The list is capped at 100 visible entries in the UI; older history is reachable via search and is always included in GDPR exports.
Automatic rollups
Profiles maintain four counters that the dashboard never has to recompute by hand:
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| visit_count | integer | — | — | Incremented when a reservation flips to status=completed via the AFTER-UPDATE trigger on reservations. |
| no_show_count | integer | — | — | Incremented on status=no_show. Surfaces in the booking widget so repeat no-shows can be challenged. |
| cancellation_count | integer | — | — | Incremented on status=cancelled. Combined with visit_count this gives the reliability score. |
| total_spent_cents | bigint | — | — | Optional — only filled when a POS / payment integration links the bill back. Used to surface “Top 10 spenders” segments. |
| first_visit_at / last_visit_at | timestamptz | — | — | Driven by the same trigger. last_visit_at sorts the Guests list so regulars surface first. |
VIP flag and blocklist
Two boolean flags on every profile let staff curate behaviour without touching reservation policies:
- is_vip — surfaces a crown badge in the reservation form whenever the email or phone matches an existing profile, lets the host pre-empt requests (favourite table, allergen reminders).
- is_blacklisted — paired with a required
blacklist_reasontext. Reservations from a blocked profile still come in (the widget can't tell), but the dashboard surfaces the block prominently before the host accepts.
Email + phone deduplication
A BEFORE-INSERT trigger on the reservations table tries to link incoming bookings to an existing profile by exact-match on email or phone (in that order). When neither matches, a fresh profile is created and the reservation is linked to it. The trigger is idempotent — re-running the same email never creates a duplicate.