Groups model a hosted trip — a group cruise, escorted tour, themed travel cohort — where one container holds many individual traveler trips, a shared roster, communication channels, and an optional public booking page that captures sign-ups directly into your CRM.
This article covers the foundation (Phase 1) through to the public booking page (Phase 3). Messages and the live chat room ship in Phase 4 — see the bottom of this article for what is coming.
When to use a Group
A Group is the right primitive when you are hosting one travel experience that multiple separately-paying parties join. Examples:
A group cruise where each couple or family books their own cabin
An escorted tour with multiple booked travelers
A themed travel cohort (LGBTQ+ pride travel, photography workshops, food + wine)
Any wedding party or anniversary trip where several rooms / cabins need their own paperwork
A Group is NOT the right primitive for a single party traveling together — that is just a Trip with multiple travelers.
Create a group
Click Groups in the sidebar (currently labeled BETA while we finish building)
Click "New group"
Enter a name (e.g. "Smith Family Cruise"), an optional default destination, optional default start/end dates, and a brand color
Submit — you land on the new group's page
The default destination and dates pre-fill onto every trip created inside the group, so your travelers do not have to re-type them.
Add members
The Members tab manages who is in the group. Two ways to add members:
Click "Add member" → search your existing org contacts → pick one. They are added with role = Member
Sign-ups via the public booking page (next section) automatically become members + get an auto-created trip
Promote a member to Leader by changing their role from the inline dropdown. Leaders get send-message permissions when Phase 4 ships.
Attach trips
Two ways to populate the group with trips:
Create new trip — opens the trip wizard with the group pre-attached; on submit the trip is automatically linked
Attach existing — search any trip in your org that is not already attached; one click moves it in
A trip can belong to multiple groups (e.g. main group cruise + pre-cruise side excursion). Detaching from one group does not affect the other.
The public booking page
Every group can have one public booking page at /g/{slug}. Open Booking page tab → click "Create booking page" if none exists, or "Edit page" if you have one.
Editor sections
Hero image — uploaded to encrypted R2 storage, 10 MB max, PNG/JPEG/WebP. Doubles as the group cover
Page title + subtitle — the hero headline
Description — plain text or simple HTML (rich editor in Phase 4)
Highlights, What's included, What's NOT included — bullet lists shown as checkmarks (highlights + included) or red Xs (excluded)
Bookable options — room types, cabin classes, package tiers. Each has a label, price, and optional sold-out toggle. Leave empty if there is one option only
Deposit amount — optional. Stored in cents (25000 = $250.00)
Terms text + required checkbox — terms are snapshotted on every submission for legal audit
Expiration — optional. After this date the public page returns 404
Publishing
Click "Publish" in the header to make the page live. Click "Unpublish" to take it down without deleting. The agent-side editor stays accessible regardless.
How sign-ups become trips automatically
Every public submission goes through this pipeline immediately on submit (no manual "convert" step):
Terms text is snapshotted onto the submission row with the IP, user agent, and timestamp
A Core contact is created (or matched on existing email / name+DOB)
A local Trips contact is created bound to that Core UUID
The primary submitter is added to the group's member roster
A Trip is created named "{Last name} party — {Group name}" with the group's default dates
Every traveler row on the submission becomes a Trip traveler (the primary is row 0 / is_primary=true)
The Trip is attached to the group via the trip-group pivot
The submission row gets status=converted with converted_to_trip_id pointing at the new Trip
If Core is unreachable during conversion the submission is still saved with status=new; the rest gets caught up on the next agent action.
Booking submissions tab
Lists every public sign-up, newest first. Columns: submitted date, primary contact, party size, selected option, status, converted Trip link. Click through to the Trip for full editing.
Broadcast messages
The Messages tab on a group sends one email to every group member who is on at least one trip in the group. Each email also lands on the recipient's trip Messages tab, so when a client replies the conversation threads back into their specific trip.
Composing
Subject + plain-text body (line breaks preserved in the email)
Pre-send count shows "Will send to N members across M trips" — and a separate amber count for members not on any trip who will be skipped
Sent via the agent's connected Gmail when available; falls back to the platform Postmark mailer otherwise (no-Gmail authors are no longer silently dropped)
Each per-trip email tagged with source_group_message_id so the broadcast can be reconstructed for audit
Past broadcasts
The right column of the Messages tab lists every broadcast in order — subject, sent date, sender, recipient count, and how many were skipped (no email on file). Click "View message" to expand the full body inline.
Why some members get skipped
Two reasons a member won't receive a broadcast:
They're a member but not on any attached trip yet — the broadcast model is per-trip, so attach them to a trip first
Their Core contact has no email on file — Core's contact-show endpoint returned an empty email. Fix by editing the Core contact and adding one
Group attachments
Files uploaded to a group live in trip_group_attachments and can be attached to any broadcast email from the composer. They're encrypted at rest on R2 with the same tenant-scoped AES-256-GCM cipher trip attachments use — so if you ever need to hand-recover a blob, the same tooling works.
Upload via the composer's "+ Upload file" button when composing a broadcast
10MB max per file. PNG / JPEG / WebP / PDFs / most common docs pass ClamAV scanning
On send, each per-trip email gets a "mirror" TripAttachment row pointing at the SAME R2 blob (no storage duplication). The mirror kind is "group-broadcast-attachment" so it doesn't appear in that trip's Files tab
Chat room (Phase 4b.1 — agent side)
The Community Chat tab on a group is a live in-app shared thread between everyone in the agency team. Polling-based delivery every 3 seconds — good enough for a booking-thread cadence, and websockets can slot in later without changing the UI contract.
How it works today
Every agent who opens the tab sees the same shared thread
Enter to send, Shift+Enter for a new line — Slack/Gmail convention
Your own messages sit on the right in brand color; teammates' on the left in slate; portal-member messages (Phase 4b.2) will render in amber
Live indicator in the header confirms polling is active. Auto-pauses when the browser tab is backgrounded, resumes when you return
Unread badge on the Community Chat tab shows the count of messages posted since your last visit
Auto-scroll pins to the bottom on new messages unless you've manually scrolled up (Slack-style)
Data model
Each message is a trip_group_chat_messages row with either author_user_id (agents) OR author_contact_id (portal members, when Phase 4b.2 lands) — never both. Per-(group, member) last-read pointers in trip_group_chat_reads drive unread counts without scanning every message.
Coming in Phase 4b.2
Portal-side chat surface — members see the same Community Chat from their trip portal and can post as themselves
Rich editor (Tiptap) for both chat and broadcast composer
Chat attachments (currently text only)
Online deposit payment (Stripe Connect) directly on the booking page
Common questions
Can a contact be in multiple groups?
Yes — the trip_group_members table is a pivot, so the same contact can be a member of many groups.
Can a trip be in multiple groups?
Yes — same M:N pattern. Most won't be, but the schema supports it.
What happens to attached trips if I archive a group?
Trips stay intact. Archiving only hides the group from the index; the trip-group pivot rows remain, so unarchiving restores the full state.
Who can access the public booking page?
Anyone with the URL — there is no auth required to sign up. Spam-control is via the IP rate limit (5 submissions/minute per IP). Add a terms_required checkbox to slow drive-by submissions further.
Related guides
Surveys
Auto-dispatch post-trip surveys, send them manually from any trip, and read results in the dashboard.
Referrals
Set up the per-agency referral program, share links from any contact, and track inquiry → booked → rewarded.
The portal sign-in prompt
What the "Sign in to your portal, or continue without signing in" splash is, when it appears on public share links, and the 7-day per-token skip cookie that keeps it from nagging clients.