Skip to content

MiniApp UI Schema Design

Version: 1.0 Created: November 30, 2025 Updated: December 1, 2025 Status: ✅ Complete (Backend + Frontend)


Implementation Status

Backend (Complete)

  • MiniAppResponse schema updated with web_ui_url field
  • BaseMiniAppHandler.get_ui_config() supports view-specific configs
  • BaseMiniAppHandler.get_web_ui_url() helper for URL generation
  • GET /miniapps/ui/{session_id}?view=X&item_id=Y API endpoint
  • POST /miniapps/action/{session_id} action handler
  • ✅ Trip Planner: overview, map, venue, share views
  • ✅ Bill Split: overview, settlement views
  • ✅ Todo List: overview view
  • ✅ Poll: overview, results views
  • ✅ Web URLs included in session creation responses
  • ✅ WebSocket endpoint /ws/miniapp/{session_id} for real-time sync

Frontend (Complete - December 2025)

  • ✅ Created /app/[appType]/[sessionId]/page.tsx in archety-web
  • ✅ Built ConfigRenderer.tsx component router
  • ✅ Implemented 15+ components (VenueCard, TaskCard, ItemCard, MapView, etc.)
  • ✅ Added API client for fetching UI configs
  • Toast notification system - success/error/warning/info toasts
  • Optimistic updates - instant UI feedback with rollback on failure
  • Real Google Maps - @react-google-maps/api with marker clustering
  • WebSocket real-time sync - multiplayer collaboration
  • Offline support - action queue with auto-sync
  • OpenGraph meta tags - rich link previews for sharing

Frontend File Structure

archety-web/
├── app/app/[appType]/[sessionId]/
│   ├── page.tsx                  # Main miniapp page
│   └── layout.tsx                # OpenGraph metadata
├── components/miniapp/
│   ├── ConfigRenderer.tsx        # Component router
│   ├── MiniAppLayout.tsx         # App shell with navigation
│   ├── OfflineIndicator.tsx      # Offline status banner
│   ├── core/                     # Core components
│   │   ├── QuickAdd.tsx
│   │   ├── FilterBar.tsx
│   │   ├── CardList.tsx
│   │   ├── ActionPanel.tsx
│   │   ├── Poll.tsx
│   │   └── ... (12 more)
│   ├── trip/                     # Trip Planner components
│   │   ├── VenueCard.tsx
│   │   ├── MapView.tsx
│   │   └── ShareCard.tsx
│   └── split/                    # Bill Split components
│       ├── ItemCard.tsx
│       ├── SplitSummary.tsx
│       ├── SettlementList.tsx
│       └── PersonCard.tsx
├── lib/miniapp/
│   ├── optimistic.ts             # Optimistic update logic
│   ├── websocket.ts              # WebSocket hook
│   └── offline.ts                # Offline queue
├── components/ui/
│   └── Toast.tsx                 # Toast notification system
└── types/miniapp.ts              # TypeScript definitions

Decisions Made

  • Authentication: Public read-only with session token in URL
  • Real-time: ✅ WebSocket sync implemented for multiplayer
  • Maps: Google Maps with graceful fallback when API key not configured
  • Offline: Action queue persisted to localStorage, auto-syncs when online

Overview

This document defines the universal UI schema for all MiniApps, enabling dynamic web pages to be generated from JSON configurations. The frontend (archety-web) will be a config-driven renderer that interprets these schemas.


Architecture Decision

Recommendation: Hybrid Approach

┌─────────────────────────────────────────────────────────────────┐
│                    archety-web (Next.js 14)                      │
│                                                                  │
│  Routes:                                                         │
│  /app/[appType]/[sessionId]/page.tsx    → Main view             │
│  /app/[appType]/[sessionId]/[view]/page.tsx → Specific views    │
│                                                                  │
│  Components:                                                     │
│  ConfigRenderer.tsx → Routes JSON to React components           │
│  Shared components for all mini-apps                            │
└─────────────────────────────────────────────────────────────────┘
                                │ GET /miniapps/ui/{session_id}?view=X
┌─────────────────────────────────────────────────────────────────┐
│                    archety-backend (FastAPI)                     │
│                                                                  │
│  Returns UIConfig JSON based on:                                │
│  - Session state (venues, tasks, items, votes)                  │
│  - Requested view (overview, map, detail, share)                │
│  - User context (permissions, votes cast, etc.)                 │
└─────────────────────────────────────────────────────────────────┘

Why Hybrid: 1. Backend already has get_ui_config() implemented for all handlers 2. Frontend renders based on JSON - no backend changes for UI tweaks 3. Single source of truth (session state) lives in backend 4. Frontend can be cached/CDN'd, backend handles business logic


Universal UI Schema

Root Schema

interface UIConfig {
  // Metadata
  app_id: string;                    // "trip_planner", "bill_split", etc.
  session_id: string;
  version: string;                   // Schema version for compatibility

  // Display
  title: string;
  theme: ThemeType;

  // Layout
  header?: HeaderConfig;
  navigation?: NavigationConfig;
  components: ComponentConfig[];
  footer?: FooterConfig;

  // Behavior
  actions?: GlobalAction[];
  refresh_interval?: number;         // Auto-refresh in seconds
  offline_capable?: boolean;

  // Sharing
  share_url?: string;
  share_config?: ShareConfig;
}

type ThemeType =
  | "travel"      // Trip Planner - purple/blue gradient
  | "finance"     // Bill Split - green accents
  | "productivity" // Todo List - clean blue
  | "voting"      // Poll - purple
  | "neutral";    // Generic

Header Config

interface HeaderConfig {
  title: string;
  subtitle?: string;

  // Progress indicator (Trip Planner, Todo)
  progress?: {
    current: number;
    total: number;
    label?: string;        // "3/10 spots visited"
  };

  // Stats row (Bill Split)
  stats?: StatItem[];

  // Dates (Trip Planner)
  dates?: {
    start?: string;        // ISO date
    end?: string;
  };

  // Hero image (Trip detail)
  image_url?: string;

  // Back navigation
  back_url?: string;
  back_label?: string;
}

interface StatItem {
  label: string;
  value: string;
  icon?: string;
}
interface NavigationConfig {
  type: "tabs" | "segmented" | "none";
  items: NavItem[];
  current: string;
}

interface NavItem {
  id: string;
  label: string;
  icon?: string;
  badge?: number;          // Notification count
  view: string;            // View to load when tapped
}

Component Library

1. Quick Add (quick_add)

Text input with action button for adding items.

interface QuickAddComponent {
  type: "quick_add";
  id: string;
  props: {
    placeholder: string;
    action: string;              // Action to trigger
    button_label?: string;       // Default: "Add"
    disabled?: boolean;
    multiline?: boolean;         // For longer input
    suggestions?: string[];      // Autocomplete suggestions
  };
}

Used by: All mini-apps


2. Filter Bar (filter_bar)

Horizontal scrolling filter chips.

interface FilterBarComponent {
  type: "filter_bar";
  id: string;
  props: {
    sections: FilterSection[];
    show_all_option?: boolean;   // Default: true
  };
}

interface FilterSection {
  id: string;
  label: string;
  options: FilterOption[];
  multi_select?: boolean;
}

interface FilterOption {
  id: string;
  label: string;
  icon?: string;
  emoji?: string;
  count?: number;
  color?: string;
}

Used by: Trip Planner (category, district, status)


3. Card List (card_list)

Grouped list of cards with sections.

interface CardListComponent {
  type: "card_list";
  id: string;
  props: {
    cards: CardItem[];
    group_by?: string;           // Field to group by
    empty_message?: string;
    show_group_headers?: boolean;
    collapsible_groups?: boolean;
  };
}

interface CardItem {
  id: string;
  type: CardType;
  data: VenueCard | TaskCard | ItemCard | PersonCard;
}

type CardType = "venue" | "task" | "item" | "person" | "option";

Used by: All mini-apps


4. Venue Card (venue_card)

Rich card for trip destinations.

interface VenueCard {
  id: string;
  name: string;
  category: string;
  category_emoji: string;

  // Location
  district?: string;
  address?: string;
  coordinates?: {
    lat: number;
    lng: number;
  };

  // Details
  vibe?: string[];             // ["cozy", "instagrammable"]
  must_try?: string[];         // ["xiaolongbao", "dandan noodles"]
  best_for?: string[];         // ["brunch", "date night"]
  price_level?: 1 | 2 | 3 | 4; // $ to $$$$

  // Status
  status: "planned" | "visited" | "skipped";
  user_rating?: 1 | 2 | 3 | 4 | 5;
  user_notes?: string;

  // Attribution
  added_by_name?: string;
  added_at?: string;
  source_url?: string;

  // Actions available
  actions?: CardAction[];
}

interface CardAction {
  action: string;
  label: string;
  icon?: string;
  style?: "primary" | "secondary" | "danger";
  confirm?: string;            // Confirmation message
}

5. Task Card (task_card)

Checkbox-based task item.

interface TaskCard {
  id: string;
  content: string;
  status: "pending" | "completed";
  priority: "high" | "normal" | "low";

  // Assignment
  assigned_to?: string;
  created_by?: string;

  // Timestamps
  created_at?: string;
  completed_at?: string;
  due_date?: string;

  // Actions
  actions?: CardAction[];
}

6. Item Card (item_card)

Bill split expense item.

interface ItemCard {
  id: string;
  description: string;
  amount: number;

  // Splitting
  shared_by?: string[];        // Person IDs
  split_type?: "equal" | "custom" | "full";
  custom_splits?: { [personId: string]: number };

  // Attribution
  paid_by?: string;
  added_by?: string;
  added_at?: string;
}

7. Person Card (person_card)

Participant in bill split.

interface PersonCard {
  id: string;
  name: string;
  avatar_url?: string;

  // Balance
  amount_paid: number;
  amount_owed: number;
  balance: number;             // Positive = gets back, negative = owes
  status: "owes" | "owed" | "settled";
}

8. Poll Component (poll)

Voting interface with results.

interface PollComponent {
  type: "poll";
  id: string;
  props: {
    question: string;
    options: PollOption[];
    total_votes: number;
    user_vote?: string;        // Option ID user voted for
    state: "collecting" | "voting" | "closed";
    anonymous?: boolean;
    show_results?: boolean;    // Show results while voting
    allow_change_vote?: boolean;
  };
}

interface PollOption {
  id: string;
  text: string;
  votes: number;
  percentage: number;
  voters?: string[];           // If not anonymous
}

9. Split Summary (split_summary)

Bill split calculation results.

interface SplitSummaryComponent {
  type: "split_summary";
  id: string;
  props: {
    total: number;
    per_person: number;
    num_people: number;
    balances: PersonBalance[];
    settlements?: Settlement[];  // Optimized payment suggestions
  };
}

interface PersonBalance {
  person_id: string;
  name: string;
  paid: number;
  owes: number;
  balance: number;
}

interface Settlement {
  from: string;
  to: string;
  amount: number;
}

10. Map View (map_view)

Interactive map with venue pins.

interface MapViewComponent {
  type: "map_view";
  id: string;
  props: {
    center: {
      lat: number;
      lng: number;
    };
    zoom: number;
    markers: MapMarker[];
    show_route?: boolean;
    cluster_markers?: boolean;
  };
}

interface MapMarker {
  id: string;
  lat: number;
  lng: number;
  label: string;
  emoji?: string;
  color?: string;
  status?: string;
  on_click?: {
    action: string;
    payload: any;
  };
}

11. Action Panel (action_panel)

Row of action buttons.

interface ActionPanelComponent {
  type: "action_panel";
  id: string;
  props: {
    actions: ActionButton[];
    layout?: "row" | "stack";
    sticky?: boolean;          // Stick to bottom
  };
}

interface ActionButton {
  action: string;
  label: string;
  icon?: string;
  style?: "primary" | "secondary" | "danger" | "outline";
  disabled?: boolean;
  loading?: boolean;
  confirm?: string;
}

12. Empty State (empty_state)

Placeholder when no data.

interface EmptyStateComponent {
  type: "empty_state";
  id: string;
  props: {
    icon?: string;
    emoji?: string;
    title: string;
    message?: string;
    action?: ActionButton;
  };
}

13. Stats Grid (stats_grid)

Grid of stat cards.

interface StatsGridComponent {
  type: "stats_grid";
  id: string;
  props: {
    stats: StatCard[];
    columns?: 2 | 3 | 4;
  };
}

interface StatCard {
  label: string;
  value: string | number;
  icon?: string;
  trend?: "up" | "down" | "neutral";
  color?: string;
}

14. Section Header (section_header)

Divider with title.

interface SectionHeaderComponent {
  type: "section_header";
  id: string;
  props: {
    title: string;
    subtitle?: string;
    action?: ActionButton;     // e.g., "See all"
    collapsible?: boolean;
    collapsed?: boolean;
  };
}

15. Info Banner (info_banner)

Informational message.

interface InfoBannerComponent {
  type: "info_banner";
  id: string;
  props: {
    type: "info" | "success" | "warning" | "error";
    title?: string;
    message: string;
    dismissible?: boolean;
    action?: ActionButton;
  };
}

View Configurations by Mini-App

Trip Planner Views

1. Overview (Default)

{
  "app_id": "trip_planner",
  "title": "Shanghai Trip",
  "theme": "travel",
  "header": {
    "title": "Shanghai",
    "subtitle": "Dec 15-22, 2025",
    "progress": {
      "current": 3,
      "total": 10,
      "label": "3/10 spots visited"
    }
  },
  "navigation": {
    "type": "tabs",
    "items": [
      { "id": "overview", "label": "List", "icon": "list", "view": "overview" },
      { "id": "map", "label": "Map", "icon": "map", "view": "map" },
      { "id": "share", "label": "Share", "icon": "share", "view": "share" }
    ],
    "current": "overview"
  },
  "components": [
    {
      "type": "quick_add",
      "id": "add_venue",
      "props": {
        "placeholder": "Add a spot (e.g., Din Tai Fung, Jing'an)",
        "action": "add_venue"
      }
    },
    {
      "type": "filter_bar",
      "id": "filters",
      "props": {
        "sections": [
          {
            "id": "category",
            "label": "Category",
            "options": [
              { "id": "restaurant", "label": "Restaurant", "emoji": "🍴", "count": 5 },
              { "id": "bar", "label": "Bar", "emoji": "🍸", "count": 2 },
              { "id": "cafe", "label": "Cafe", "emoji": "☕", "count": 3 }
            ]
          },
          {
            "id": "district",
            "label": "District",
            "options": [
              { "id": "jingan", "label": "Jing'an", "count": 4 },
              { "id": "french-concession", "label": "French Concession", "count": 3 }
            ]
          },
          {
            "id": "status",
            "label": "Status",
            "options": [
              { "id": "planned", "label": "Planned", "count": 7 },
              { "id": "visited", "label": "Visited", "count": 3 }
            ]
          }
        ]
      }
    },
    {
      "type": "card_list",
      "id": "venues",
      "props": {
        "cards": [],
        "group_by": "district",
        "show_group_headers": true,
        "empty_message": "No venues yet! Add spots by typing above."
      }
    }
  ]
}

2. Map View

{
  "app_id": "trip_planner",
  "title": "Shanghai Trip - Map",
  "theme": "travel",
  "header": {
    "title": "Shanghai",
    "subtitle": "10 spots"
  },
  "navigation": {
    "type": "tabs",
    "items": [
      { "id": "overview", "label": "List", "view": "overview" },
      { "id": "map", "label": "Map", "view": "map" }
    ],
    "current": "map"
  },
  "components": [
    {
      "type": "map_view",
      "id": "trip_map",
      "props": {
        "center": { "lat": 31.2304, "lng": 121.4737 },
        "zoom": 12,
        "markers": [
          {
            "id": "venue_1",
            "lat": 31.2244,
            "lng": 121.4555,
            "label": "Din Tai Fung",
            "emoji": "🍴",
            "status": "planned"
          }
        ],
        "cluster_markers": true
      }
    },
    {
      "type": "card_list",
      "id": "venue_preview",
      "props": {
        "cards": [],
        "empty_message": "Tap a pin to see details"
      }
    }
  ]
}

3. Venue Detail View

{
  "app_id": "trip_planner",
  "title": "Din Tai Fung",
  "theme": "travel",
  "header": {
    "title": "Din Tai Fung",
    "subtitle": "Jing'an • Restaurant",
    "image_url": "https://...",
    "back_url": "/app/trip/abc123",
    "back_label": "Back to trip"
  },
  "components": [
    {
      "type": "stats_grid",
      "id": "venue_stats",
      "props": {
        "stats": [
          { "label": "Category", "value": "🍴 Restaurant" },
          { "label": "District", "value": "Jing'an" },
          { "label": "Price", "value": "$$" },
          { "label": "Status", "value": "Planned" }
        ]
      }
    },
    {
      "type": "section_header",
      "id": "vibe_header",
      "props": { "title": "Vibe" }
    },
    {
      "type": "tag_list",
      "id": "vibe_tags",
      "props": {
        "tags": ["cozy", "instagrammable", "date-night"]
      }
    },
    {
      "type": "section_header",
      "id": "must_try_header",
      "props": { "title": "Must Try" }
    },
    {
      "type": "bullet_list",
      "id": "must_try",
      "props": {
        "items": ["Xiaolongbao", "Dandan Noodles", "Shrimp Fried Rice"]
      }
    },
    {
      "type": "section_header",
      "id": "notes_header",
      "props": { "title": "Notes" }
    },
    {
      "type": "text_block",
      "id": "notes",
      "props": {
        "text": "Make reservations 2 days ahead. Best to go for lunch.",
        "editable": true,
        "action": "update_notes"
      }
    },
    {
      "type": "action_panel",
      "id": "actions",
      "props": {
        "actions": [
          { "action": "mark_visited", "label": "Mark Visited", "style": "primary", "icon": "check" },
          { "action": "get_directions", "label": "Directions", "style": "secondary", "icon": "navigation" },
          { "action": "delete_venue", "label": "Remove", "style": "danger", "icon": "trash", "confirm": "Remove this venue?" }
        ],
        "sticky": true
      }
    }
  ]
}

4. Share View

{
  "app_id": "trip_planner",
  "title": "Share Trip",
  "theme": "travel",
  "header": {
    "title": "Share Your Trip",
    "subtitle": "Shanghai • 10 spots"
  },
  "components": [
    {
      "type": "info_banner",
      "id": "share_info",
      "props": {
        "type": "info",
        "message": "Anyone with this link can view your trip (read-only)"
      }
    },
    {
      "type": "share_card",
      "id": "share_link",
      "props": {
        "url": "https://app.archety.ai/trip/abc123/share",
        "title": "Shanghai Trip",
        "description": "10 spots to visit",
        "show_qr": true,
        "copy_action": "copy_share_link"
      }
    },
    {
      "type": "action_panel",
      "id": "share_actions",
      "props": {
        "actions": [
          { "action": "copy_link", "label": "Copy Link", "icon": "copy" },
          { "action": "share_native", "label": "Share", "icon": "share" },
          { "action": "export_pdf", "label": "Export PDF", "icon": "download" }
        ]
      }
    }
  ]
}

Bill Split Views

1. Overview (Default)

{
  "app_id": "bill_split",
  "title": "Dinner Split",
  "theme": "finance",
  "header": {
    "title": "Dinner at Izakaya",
    "stats": [
      { "label": "Total", "value": "$156.00" },
      { "label": "People", "value": "4" },
      { "label": "Per Person", "value": "$39.00" }
    ]
  },
  "components": [
    {
      "type": "quick_add",
      "id": "add_item",
      "props": {
        "placeholder": "Add item (e.g., Pizza $25)",
        "action": "add_item"
      }
    },
    {
      "type": "section_header",
      "id": "items_header",
      "props": {
        "title": "Items",
        "subtitle": "5 items"
      }
    },
    {
      "type": "card_list",
      "id": "items",
      "props": {
        "cards": [
          {
            "id": "item_1",
            "type": "item",
            "data": {
              "id": "item_1",
              "description": "Edamame",
              "amount": 8.00,
              "shared_by": ["alice", "bob", "charlie", "diana"]
            }
          }
        ],
        "empty_message": "No items yet"
      }
    },
    {
      "type": "section_header",
      "id": "people_header",
      "props": {
        "title": "People",
        "action": { "action": "add_person", "label": "+ Add", "style": "outline" }
      }
    },
    {
      "type": "split_summary",
      "id": "split",
      "props": {
        "total": 156.00,
        "per_person": 39.00,
        "num_people": 4,
        "balances": [
          { "person_id": "alice", "name": "Alice", "paid": 156.00, "owes": 39.00, "balance": 117.00 },
          { "person_id": "bob", "name": "Bob", "paid": 0, "owes": 39.00, "balance": -39.00 },
          { "person_id": "charlie", "name": "Charlie", "paid": 0, "owes": 39.00, "balance": -39.00 },
          { "person_id": "diana", "name": "Diana", "paid": 0, "owes": 39.00, "balance": -39.00 }
        ],
        "settlements": [
          { "from": "Bob", "to": "Alice", "amount": 39.00 },
          { "from": "Charlie", "to": "Alice", "amount": 39.00 },
          { "from": "Diana", "to": "Alice", "amount": 39.00 }
        ]
      }
    },
    {
      "type": "action_panel",
      "id": "actions",
      "props": {
        "actions": [
          { "action": "calculate", "label": "Recalculate", "icon": "calculator" },
          { "action": "share_split", "label": "Share Split", "icon": "share", "style": "primary" }
        ],
        "sticky": true
      }
    }
  ]
}

2. Settlement View

{
  "app_id": "bill_split",
  "title": "Settlement",
  "theme": "finance",
  "header": {
    "title": "Who Pays Who",
    "subtitle": "$156.00 total"
  },
  "components": [
    {
      "type": "settlement_list",
      "id": "settlements",
      "props": {
        "settlements": [
          {
            "from": { "id": "bob", "name": "Bob" },
            "to": { "id": "alice", "name": "Alice" },
            "amount": 39.00,
            "status": "pending"
          }
        ],
        "allow_mark_paid": true
      }
    },
    {
      "type": "action_panel",
      "id": "actions",
      "props": {
        "actions": [
          { "action": "send_reminders", "label": "Send Reminders", "icon": "bell" },
          { "action": "mark_all_settled", "label": "All Settled", "style": "primary" }
        ]
      }
    }
  ]
}

Todo List Views

1. Overview (Default)

{
  "app_id": "todo_list",
  "title": "Project Tasks",
  "theme": "productivity",
  "header": {
    "title": "Project Tasks",
    "progress": {
      "current": 5,
      "total": 12,
      "label": "5/12 completed"
    }
  },
  "components": [
    {
      "type": "quick_add",
      "id": "add_task",
      "props": {
        "placeholder": "Add a task (! for high priority)",
        "action": "add_task"
      }
    },
    {
      "type": "filter_bar",
      "id": "filters",
      "props": {
        "sections": [
          {
            "id": "status",
            "label": "Status",
            "options": [
              { "id": "pending", "label": "To Do", "count": 7 },
              { "id": "completed", "label": "Done", "count": 5 }
            ]
          },
          {
            "id": "priority",
            "label": "Priority",
            "options": [
              { "id": "high", "label": "High", "color": "red", "count": 2 },
              { "id": "normal", "label": "Normal", "count": 8 },
              { "id": "low", "label": "Low", "color": "blue", "count": 2 }
            ]
          }
        ]
      }
    },
    {
      "type": "section_header",
      "id": "pending_header",
      "props": {
        "title": "To Do",
        "subtitle": "7 tasks"
      }
    },
    {
      "type": "card_list",
      "id": "pending_tasks",
      "props": {
        "cards": [],
        "empty_message": "All done! 🎉"
      }
    },
    {
      "type": "section_header",
      "id": "completed_header",
      "props": {
        "title": "Completed",
        "subtitle": "5 tasks",
        "collapsible": true,
        "collapsed": true
      }
    },
    {
      "type": "card_list",
      "id": "completed_tasks",
      "props": {
        "cards": []
      }
    }
  ]
}

Poll Views

1. Voting View (Default)

{
  "app_id": "poll",
  "title": "Team Vote",
  "theme": "voting",
  "header": {
    "title": "Where should we eat?",
    "subtitle": "8 votes • Voting open"
  },
  "components": [
    {
      "type": "poll",
      "id": "poll_options",
      "props": {
        "question": "Where should we eat?",
        "options": [
          { "id": "opt1", "text": "Italian", "votes": 3, "percentage": 37.5 },
          { "id": "opt2", "text": "Japanese", "votes": 4, "percentage": 50 },
          { "id": "opt3", "text": "Mexican", "votes": 1, "percentage": 12.5 }
        ],
        "total_votes": 8,
        "user_vote": "opt2",
        "state": "voting",
        "show_results": true
      }
    },
    {
      "type": "quick_add",
      "id": "add_option",
      "props": {
        "placeholder": "Suggest another option...",
        "action": "add_option",
        "disabled": false
      }
    },
    {
      "type": "action_panel",
      "id": "actions",
      "props": {
        "actions": [
          { "action": "close_poll", "label": "Close Voting", "style": "primary" },
          { "action": "share_poll", "label": "Share Poll", "icon": "share" }
        ]
      }
    }
  ]
}

2. Results View

{
  "app_id": "poll",
  "title": "Poll Results",
  "theme": "voting",
  "header": {
    "title": "Where should we eat?",
    "subtitle": "Poll closed • 8 votes"
  },
  "components": [
    {
      "type": "info_banner",
      "id": "winner_banner",
      "props": {
        "type": "success",
        "title": "Winner: Japanese",
        "message": "4 votes (50%)"
      }
    },
    {
      "type": "poll",
      "id": "poll_results",
      "props": {
        "question": "Where should we eat?",
        "options": [
          { "id": "opt2", "text": "Japanese", "votes": 4, "percentage": 50, "winner": true },
          { "id": "opt1", "text": "Italian", "votes": 3, "percentage": 37.5 },
          { "id": "opt3", "text": "Mexican", "votes": 1, "percentage": 12.5 }
        ],
        "total_votes": 8,
        "state": "closed",
        "show_voters": true
      }
    }
  ]
}

URL Structure

Frontend Routes (archety-web)

/app/trip/[sessionId]              → Trip overview
/app/trip/[sessionId]/map          → Map view
/app/trip/[sessionId]/venue/[id]   → Venue detail
/app/trip/[sessionId]/share        → Shareable view

/app/split/[sessionId]             → Bill split overview
/app/split/[sessionId]/settle      → Settlement view

/app/todo/[sessionId]              → Todo list

/app/poll/[sessionId]              → Poll voting
/app/poll/[sessionId]/results      → Poll results

Backend API

GET  /miniapps/ui/{session_id}                    → Default view config
GET  /miniapps/ui/{session_id}?view=map           → Specific view
GET  /miniapps/ui/{session_id}?view=venue&id=xyz  → Item detail view
POST /miniapps/action/{session_id}                → Handle actions

Actions Reference

Trip Planner Actions

Action Payload Description
add_venue { raw_input: string } Add venue from text
mark_visited { venue_id: string, rating?: number, notes?: string } Mark as visited
update_venue { venue_id: string, updates: object } Update venue details
delete_venue { venue_id: string } Remove venue
set_filter { filter_type: string, value: string } Apply filter
copy_share_link {} Copy shareable link

Bill Split Actions

Action Payload Description
add_item { raw_input: string } Add expense item
add_person { name: string } Add participant
record_payment { person_id: string, amount: number } Record payment
calculate {} Recalculate splits
mark_settled { from: string, to: string } Mark settlement done

Todo List Actions

Action Payload Description
add_task { raw_input: string } Add task
complete_task { task_id: string } Mark complete
delete_task { task_id: string } Delete task
set_priority { task_id: string, priority: string } Change priority
assign_task { task_id: string, assignee: string } Assign to person

Poll Actions

Action Payload Description
add_option { raw_input: string } Add poll option
vote { option_id: string } Cast vote
start_voting {} Open voting
close_poll {} Close and show results

Implementation Phases

Phase 1: Core Infrastructure ✅ COMPLETE

  • Create /app/[appType]/[sessionId]/page.tsx in archety-web
  • Build ConfigRenderer.tsx component router
  • Implement base components: Header, QuickAdd, ActionPanel
  • Add API client for fetching UI configs

Phase 2: Trip Planner UI ✅ COMPLETE

  • VenueCard component
  • FilterBar component
  • CardList with grouping
  • Map view with Google Maps

Phase 3: Other Mini-Apps ✅ COMPLETE

  • TaskCard component
  • Poll component
  • SplitSummary component
  • SettlementList component

Phase 4: Polish & Sharing ✅ COMPLETE

  • Share view with QR codes
  • PDF export (future enhancement)
  • Deep linking from chat
  • Offline support with action queue

Open Questions (Resolved)

  1. Authentication: How do we handle auth for web views?
  2. Decision: Public read-only with session token in URL
  3. Session ID in URL acts as capability token
  4. No login required to view
  5. Future: Add write authentication if needed

  6. Real-time Updates: Should we use WebSockets for live updates?

  7. Decision: Not initially - refreshes are acceptable
  8. Users can refresh to see updates
  9. Simpler implementation, faster to ship
  10. Can add WebSockets later if needed

  11. Offline Mode: Which mini-apps need offline support?

  12. Implemented: Action queue with auto-sync
  13. All mini-apps queue actions when offline
  14. Auto-sync when connection restored
  15. Persisted to localStorage via Zustand

  16. Map Provider: Mapbox vs Google Maps?

  17. Decision: Google Maps
  18. Implemented with @react-google-maps/api
  19. Graceful fallback when API key not configured
  20. Marker clustering for dense venues