wedding-concierge
AI Wedding Guest Concierge — Technical Architecture
Overview
An AI-powered concierge that lets wedding guests chat with a knowledgeable assistant about travel, accommodation, schedule, RSVPs, and dietary needs. The couple configures their wedding details once; guests each get a unique link. The system is designed to run as a local CLI tool first (Lana runs it on her machine), with a clear path to a deployed Cloudflare version later.
COUPLE GUESTS LANA (operator)
| | |
| 1. Edit wedding.yaml | 3. Visit unique link |
| 2. Add guest list | /guest/abc123 |
| | 4. Chat with concierge |
v v v
+------------------+ +-------------------+ +------------------+
| Wedding Config | | Chat Interface | | Dashboard |
| (YAML/JSON) | | (Astro page) | | (CLI or page) |
+--------+---------+ +--------+----------+ +--------+---------+
| | |
+----------+------------+------------------------+
|
+---------v----------+
| Claude Code CLI |
| (router + brain) |
+---------+----------+
|
+---------v----------+
| File-Based Store |
| (JSON + Markdown) |
+--------------------+
Phase 1: Local CLI MVP (build this first)
The simplest thing that works today. Lana runs it on her laptop. No server, no deployment, no Cloudflare. Claude Code CLI is the entire backend.
Data Model — File-Based Store
All data lives in a wedding workspace directory:
weddings/
aaron-lana-2026/
wedding.yaml # Wedding config (couple edits this)
guests/
abc123.json # Guest record (one per guest)
def456.json
conversations/
abc123/
2026-03-08.md # Chat transcript per day
def456/
2026-03-08.md
dashboard/
summary.json # Aggregated guest data (generated)
rsvps.json # RSVP roll-up
dietary.json # Dietary needs roll-up
wedding.yaml
wedding:
couple: "Aaron & Lana"
date: "2026-08-15"
ceremony_time: "3:00 PM"
location:
venue: "Brackendale Art Gallery"
address: "41950 Government Rd, Brackendale, BC"
coordinates: [49.7680, -123.1558]
nearest_airport: "YVR (Vancouver International)"
drive_from_airport: "~90 min via Sea-to-Sky Highway"
schedule:
- time: "2:00 PM"
event: "Guest arrival"
notes: "Parking available on-site"
- time: "3:00 PM"
event: "Ceremony"
location: "Outdoor garden (weather permitting)"
- time: "4:00 PM"
event: "Cocktail hour"
- time: "5:30 PM"
event: "Dinner"
- time: "8:00 PM"
event: "Dancing"
- time: "11:00 PM"
event: "Last call"
accommodation:
- name: "Sunwolf Riverside Resort"
url: "https://sunwolf.net"
distance: "5 min drive"
price_range: "$150-250/night"
notes: "Riverside cabins, very close to venue"
- name: "Executive Suites Squamish"
url: "https://executivesuites.com"
distance: "15 min drive"
price_range: "$180-300/night"
notes: "Hotel suites, good for families"
activities:
- name: "Sea to Sky Gondola"
url: "https://seatoskygondola.com"
when: "Day before or after"
notes: "Stunning views, ~20 min from venue"
- name: "Stawamus Chief hike"
difficulty: "Moderate-hard"
when: "Morning of (for the adventurous)"
- name: "Brackendale Eagles"
when: "Winter only"
notes: "If wedding is Nov-Feb"
logistics:
dress_code: "Garden party — smart casual, comfortable shoes"
shuttle: "Shuttle running between Sunwolf and venue 1:30-midnight"
parking: "Free parking at venue, limited spots"
weather_backup: "Indoor gallery space if rain"
registry_url: "https://example.com/registry"
concierge:
voice: >
Warm, helpful, and enthusiastic about the wedding. You know all the
details and genuinely want each guest to have a wonderful experience.
Be concise but thorough. If you don't know something, say so and
suggest the guest contact Lana directly.
couple_contact: "lana@example.com"
Guest Record (guests/abc123.json)
{
"id": "abc123",
"name": "Jordan Smith",
"email": "jordan@example.com",
"party_size": 2,
"plus_one_name": "Alex Chen",
"rsvp": {
"status": "attending",
"responded_at": "2026-04-15T10:30:00Z"
},
"dietary": {
"jordan": "vegetarian",
"alex": "no restrictions"
},
"accommodation": {
"booked": true,
"where": "Sunwolf Riverside Resort",
"check_in": "2026-08-14",
"check_out": "2026-08-16"
},
"travel": {
"arriving_from": "Toronto",
"flight": "AC123 arriving Aug 14 at 2pm",
"needs_shuttle": true
},
"notes": "Asked about bringing dog — told no pets at venue",
"last_interaction": "2026-04-15T10:45:00Z"
}
CLI Workflow
Lana interacts entirely through Claude Code CLI:
# Start the concierge for a wedding
claude "Load wedding config from weddings/aaron-lana-2026/wedding.yaml
and process any new guest messages."
# Process a single guest interaction (simulating what the web version does)
claude "You are the wedding concierge for Aaron & Lana's wedding.
Guest abc123 (Jordan Smith) asks: 'What should I wear?'
Read wedding.yaml for context and their guest file for history.
Respond and update their record if needed."
# Generate dashboard
claude "Read all guest files in weddings/aaron-lana-2026/guests/
and generate a summary dashboard."
Concierge Skill
This fits naturally as a Claude Code skill in the monorepo:
.claude/skills/wedding-concierge/SKILL.md
The skill would:
- Load the wedding config
- Load the guest’s history
- Build a system prompt with full wedding context
- Process the guest’s message
- Update the guest record (RSVP, dietary, accommodation, etc.)
- Append to the conversation transcript
- Return the response
System Prompt Construction
The concierge’s system prompt is assembled dynamically:
+---------------------------+
| Base concierge persona | <- from wedding.yaml > concierge.voice
+---------------------------+
| Wedding details | <- from wedding.yaml (full config)
+---------------------------+
| This guest's context | <- from guests/{id}.json
+---------------------------+
| Conversation history | <- from conversations/{id}/*.md
+---------------------------+
| Tool instructions | <- what structured data to extract
+---------------------------+
= Complete system prompt for this interaction
Structured Data Extraction
The concierge extracts structured data from natural conversation. When a guest says “I’m vegetarian and my partner is gluten-free,” the concierge both responds naturally AND updates the guest record. This is done through a simple instruction in the system prompt:
When a guest provides any of the following information, update their
guest record JSON file:
- RSVP status (attending / not attending / maybe)
- Dietary restrictions (per person in their party)
- Accommodation plans
- Travel details (arrival, departure, transport needs)
- Plus-one information
- Special requests or accessibility needs
After each interaction, append to the conversation log and update the
guest JSON if any structured data was mentioned.
This works because Claude Code CLI can read AND write files directly.
Phase 2: Web Chat Interface (Astro + Cloudflare Functions)
Architecture
GUEST BROWSER
|
| HTTPS
v
+------------------------+
| Cloudflare Pages |
| (Astro static site) |
| |
| /guest/[token] | <- Static chat page
| /couple/dashboard | <- Static dashboard
+------------------------+
|
Cloudflare Functions (server-side)
/api/chat /api/rsvp /api/dashboard
|
+-----------v------------+
| Cloudflare Workers |
| |
| 1. Validate token |
| 2. Load guest context |
| 3. Call Claude API |
| 4. Store response |
| 5. Return to client |
+-----------+------------+
|
+-----------v------------+
| Data Storage |
| |
| KV: guest sessions, |
| chat history |
| D1: structured data |
| (RSVPs, dietary) |
+------------------------+
Why Cloudflare Functions (not a separate API)
Astro 5 on Cloudflare Pages supports server endpoints via the
@astrojs/cloudflare adapter. This means API routes live in the same
project — no separate Worker to deploy. The routes are:
src/pages/api/chat.ts POST — send message, get response
src/pages/api/rsvp.ts POST — submit/update RSVP
src/pages/api/guest/[token].ts GET — load guest context
src/pages/api/dashboard.ts GET — aggregated data for couple
Chat Interface
Simple, no WebSockets needed. Use standard fetch with streaming:
GUEST SERVER
| |
| POST /api/chat |
| { token, message } |
|------------------------------>|
| | 1. Validate token against KV
| | 2. Load guest record from KV/D1
| | 3. Load wedding config
| | 4. Build system prompt
| | 5. Call Claude API (streaming)
| |
| ReadableStream (SSE) |
|<------------------------------| 6. Stream response tokens
| | 7. On complete: store in KV
| (renders incrementally) |
| |
Why streaming over SSE instead of WebSockets:
- Cloudflare Workers support ReadableStream responses natively
- No connection management overhead
- Each message is a stateless request (context loaded from storage)
- Good enough UX for a wedding concierge (not a real-time collaboration tool)
Client-side implementation:
// Minimal chat client — no framework needed, just vanilla JS
// in an Astro page's <script> tag
async function sendMessage(message: string) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: GUEST_TOKEN, message }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
appendToChat(decoder.decode(value));
}
}
Data Storage Strategy
Cloudflare KV (key-value, eventually consistent):
- Guest tokens -> guest records
- Chat history per guest (last N messages for context window)
- Wedding config (cached, rarely changes)
Cloudflare D1 (SQLite, strongly consistent):
- RSVPs (need accurate counts)
- Dietary requirements (need reliable aggregation)
- Guest list (relational: guest -> party members)
Why both? KV is fast and cheap for chat history (append-heavy, read-heavy, eventual consistency is fine). D1 is needed for the dashboard queries where you want accurate aggregates (“how many vegetarians total?”).
KV key structure:
wedding:{weddingId}:config -> wedding YAML as JSON
wedding:{weddingId}:guest:{token} -> guest record JSON
wedding:{weddingId}:chat:{token} -> last 50 messages JSON array
wedding:{weddingId}:guest-list -> array of all guest tokens
D1 schema (minimal):
CREATE TABLE guests (
token TEXT PRIMARY KEY,
wedding_id TEXT NOT NULL,
name TEXT NOT NULL,
email TEXT,
party_size INTEGER DEFAULT 1,
rsvp TEXT DEFAULT 'pending', -- pending | attending | declined
rsvp_at TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE dietary (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guest_token TEXT REFERENCES guests(token),
person_name TEXT NOT NULL,
restriction TEXT NOT NULL
);
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guest_token TEXT REFERENCES guests(token),
role TEXT NOT NULL, -- user | assistant
content TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
Guest Token Design
Each guest gets a unique, unguessable URL:
https://weddingslana.com/guest/k7x9m2p4 (8 chars, base36)
- No login required (the token IS the auth)
- Tokens are generated when the couple uploads the guest list
- Short enough to text/email, long enough to be unguessable
- Can be revoked by deleting from KV
Token generation:
import { nanoid } from 'nanoid';
const token = nanoid(10); // e.g., "V1StGXR8_Z"
Couple Dashboard
A protected page (simple password or Cloudflare Access) showing:
+----------------------------------------------------------+
| Aaron & Lana's Wedding — Guest Dashboard |
+----------------------------------------------------------+
| |
| RSVPs: 42 attending | 8 declined | 12 pending |
| Total guests (with +1s): 67 |
| |
| Dietary: |
| 12 vegetarian | 4 vegan | 3 gluten-free | 2 kosher |
| |
| Accommodation: |
| 18 at Sunwolf | 12 at Executive Suites | 15 unknown |
| |
| Shuttle: |
| 28 need shuttle | 14 driving | 20 unknown |
| |
+----------------------------------------------------------+
| Recent Conversations |
| - Jordan Smith (2h ago): Asked about parking |
| - Pat Lee (yesterday): Updated dietary to vegan |
| - Casey Ng (2d ago): RSVPd yes, party of 3 |
+----------------------------------------------------------+
| Open Questions (flagged by concierge) |
| - Morgan asks: "Can we bring our 6-month-old?" |
| - Riley asks: "Is there a plan B if it rains?" |
+----------------------------------------------------------+
In Phase 1 (CLI), this is generated as a markdown file. In Phase 2 (web), it’s an Astro page hitting the /api/dashboard endpoint.
Phase 3: Multi-Wedding Platform (future)
Not designed in detail, but the architecture supports it:
- Each wedding gets its own namespace in KV/D1
- The
weddings-lanasite becomes a landing page + wedding selector - Couples get their own dashboard scoped to their wedding
- Concierge persona is fully configurable per wedding
Integration with the Monorepo
Where it lives
apps/weddings-lana/ # Existing site
src/pages/
guest/[token].astro # Chat page (Phase 2)
couple/dashboard.astro # Dashboard (Phase 2)
api/chat.ts # Chat endpoint (Phase 2)
api/rsvp.ts # RSVP endpoint (Phase 2)
api/dashboard.ts # Dashboard data (Phase 2)
.claude/skills/wedding-concierge/ # CLI skill (Phase 1)
SKILL.md
weddings/ # Wedding data (Phase 1, gitignored)
aaron-lana-2026/
wedding.yaml
guests/
conversations/
dashboard/
Astro Config Changes (Phase 2)
// astro.config.mjs — add Cloudflare adapter for server endpoints
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
output: 'hybrid', // static pages + server API routes
adapter: cloudflare({
platformProxy: { enabled: true },
}),
vite: {
plugins: [tailwindcss()],
},
});
output: 'hybrid' means most pages stay static (fast, cached at edge)
while API routes run as Cloudflare Functions.
Claude API Integration (Phase 2)
The Cloudflare Worker calls Claude’s API directly (not Claude Code CLI):
// src/pages/api/chat.ts
import Anthropic from '@anthropic-ai/sdk';
export const POST: APIRoute = async ({ request, locals }) => {
const { token, message } = await request.json();
// 1. Load context
const guest = await locals.runtime.env.KV.get(`wedding:...:guest:${token}`, 'json');
const config = await locals.runtime.env.KV.get(`wedding:...:config`, 'json');
const history = await locals.runtime.env.KV.get(`wedding:...:chat:${token}`, 'json') || [];
// 2. Build messages
const systemPrompt = buildConciergePrompt(config, guest);
const messages = [
...history.slice(-20), // last 20 messages for context
{ role: 'user', content: message },
];
// 3. Stream response
const client = new Anthropic({ apiKey: locals.runtime.env.CLAUDE_API_KEY });
const stream = client.messages.stream({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
system: systemPrompt,
messages,
});
// 4. Return streaming response
return new Response(stream.toReadableStream(), {
headers: { 'Content-Type': 'text/event-stream' },
});
};
Cost Estimation
Per guest interaction (assuming ~500 token prompt + wedding context, ~300 token response):
- Input: ~2,000 tokens (system + history + message) = ~$0.006
- Output: ~300 tokens = ~$0.0045
- Per message: ~$0.01
- Per guest (avg 5 messages): ~$0.05
- 100-guest wedding: ~$5 total API cost
Very affordable. Sonnet is the right model — fast, cheap, good enough.
Security Considerations
-
Guest tokens are bearer tokens. Anyone with the URL can chat. This is acceptable for a wedding (low-stakes, guests share links anyway). But tokens should not be guessable (use nanoid, not sequential IDs).
-
Rate limiting. Add per-token rate limits in the Worker to prevent abuse (e.g., max 20 messages per hour per token).
-
API key protection. Claude API key lives in Cloudflare environment variables (Workers Secrets), never in client code.
-
No PII in logs. Chat history stays in KV/D1, not in application logs.
-
Dashboard auth. Use Cloudflare Access (free for up to 50 users) or a simple shared password for the couple’s dashboard.
-
Concierge guardrails. The system prompt should instruct Claude to:
- Only discuss wedding-related topics
- Never share other guests’ personal information
- Redirect sensitive questions to the couple directly
- Not make commitments the couple hasn’t authorized
Build Plan
Phase 1 — Local CLI MVP (1-2 days)
[ ] Create wedding.yaml schema and example config
[ ] Create guest JSON schema
[ ] Write .claude/skills/wedding-concierge/SKILL.md
[ ] Build CLI workflow: process guest message -> update record
[ ] Build CLI workflow: generate dashboard summary
[ ] Test with 5 fake guests
What Lana gets: She can run Claude Code on her laptop, paste in a guest’s question, get a response to send them, and see aggregated data. Manual but functional.
Phase 2a — Web Chat (3-5 days)
[ ] Add @astrojs/cloudflare adapter to weddings-lana
[ ] Build /guest/[token].astro chat page (vanilla JS, no framework)
[ ] Build POST /api/chat endpoint with Claude API streaming
[ ] Set up KV namespace and D1 database
[ ] Build guest token generation script
[ ] Build /api/rsvp endpoint
[ ] Test end-to-end with real Claude API
What Lana gets: Guests can actually chat via a web page. She doesn’t need to be in the loop for every question.
Phase 2b — Dashboard (2-3 days)
[ ] Build /couple/dashboard.astro page
[ ] Build /api/dashboard endpoint (aggregate from D1)
[ ] Add Cloudflare Access for dashboard auth
[ ] Add "flagged questions" feature (concierge escalates to couple)
[ ] Add export to CSV for caterer/venue
Phase 3 — Polish (ongoing)
[ ] Guest onboarding flow (first visit collects name, RSVP)
[ ] Email/SMS notifications when guests RSVP
[ ] Multi-wedding support
[ ] Custom branding per wedding
Key Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Phase 1 backend | Claude Code CLI | Zero infrastructure, works today, Lana already uses it |
| Phase 2 backend | Cloudflare Functions | Same project as frontend, no separate API to deploy |
| Chat protocol | Streaming fetch (SSE) | Simpler than WebSockets, Workers support it natively |
| Chat context | Last 20 messages | Keeps API costs low, sufficient for wedding Q&A |
| Structured data extraction | In-prompt instructions | No tool use needed, Claude reliably extracts from conversation |
| Guest auth | Bearer token in URL | No login friction for wedding guests |
| Primary storage | KV (chat) + D1 (structured) | KV for speed, D1 for accurate aggregation |
| Model | claude-sonnet | Fast, cheap ($0.01/msg), more than capable for Q&A |
| Astro output mode | hybrid | Static pages + server API routes in one project |
Comparison: CLI vs Web
Phase 1 (CLI) Phase 2 (Web)
───────────── ─────────────
Guest interface Lana copy-pastes Self-service chat page
Backend Claude Code CLI Cloudflare Functions + API
Storage Local JSON/YAML files Cloudflare KV + D1
Dashboard Generated markdown Live web page
Concierge brain Claude Code CLI Claude API (direct)
Cost $0 (CLI usage) ~$5/wedding (API calls)
Availability When Lana's laptop is on 24/7
Setup effort 1-2 days 1 week
The CLI phase is not throwaway work. The wedding config schema, system prompt design, and data extraction logic all carry forward into Phase 2. The main change is swapping “Claude Code reads/writes files” for “Cloudflare Worker reads/writes KV/D1.”