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:

  1. Load the wedding config
  2. Load the guest’s history
  3. Build a system prompt with full wedding context
  4. Process the guest’s message
  5. Update the guest record (RSVP, dietary, accommodation, etc.)
  6. Append to the conversation transcript
  7. 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-lana site 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

  1. 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).

  2. Rate limiting. Add per-token rate limits in the Worker to prevent abuse (e.g., max 20 messages per hour per token).

  3. API key protection. Claude API key lives in Cloudflare environment variables (Workers Secrets), never in client code.

  4. No PII in logs. Chat history stays in KV/D1, not in application logs.

  5. Dashboard auth. Use Cloudflare Access (free for up to 50 users) or a simple shared password for the couple’s dashboard.

  6. 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

DecisionChoiceRationale
Phase 1 backendClaude Code CLIZero infrastructure, works today, Lana already uses it
Phase 2 backendCloudflare FunctionsSame project as frontend, no separate API to deploy
Chat protocolStreaming fetch (SSE)Simpler than WebSockets, Workers support it natively
Chat contextLast 20 messagesKeeps API costs low, sufficient for wedding Q&A
Structured data extractionIn-prompt instructionsNo tool use needed, Claude reliably extracts from conversation
Guest authBearer token in URLNo login friction for wedding guests
Primary storageKV (chat) + D1 (structured)KV for speed, D1 for accurate aggregation
Modelclaude-sonnetFast, cheap ($0.01/msg), more than capable for Q&A
Astro output modehybridStatic 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.”