Skip to content

ADR 002: Standardized UI Blocks Over Custom UIs for Mini-Apps

Date: 2025-11-10 Status: Proposed Deciders: Engineering Team, Product Lead Related: Constitution, System Overview


Context

Gen Z mini-app ecosystem requires rich, interactive experiences for multiplayer coordination (voting, lists, galleries, forms). We need to decide how to render these interfaces.

Requirements

  1. Platform Support: Must work in iMessage (primary) and Telegram
  2. Zero Friction: No app downloads or web views
  3. Conversational: Feels like chat, not a separate app
  4. Consistent UX: All mini-apps have similar interaction patterns
  5. Developer Velocity: Easy to build new mini-apps
  6. Accessibility: Screen readers, VoiceOver compatible

Challenge

iMessage and Telegram have limited UI capabilities compared to native apps: - No custom HTML/CSS rendering - No JavaScript execution - Text + images + basic formatting only - Interaction via message sends, not button clicks (in iMessage)


Decision

We will use 10 Standardized UI Blocks that render natively in messaging platforms.

Each block: 1. Has a JSON schema defining its data structure 2. Has platform-specific renderers (iMessage formatter, Telegram formatter) 3. Supports interaction handlers (user taps → events) 4. Is composable (blocks can contain other blocks)

Principle: Mini-apps are composed entirely from these 10 blocks. No custom UIs.


UI Block Catalog

1. Text Block

Purpose: Simple text message with optional formatting

Schema:

{
  "type": "text",
  "content": "Your trip to Tokyo is shaping up! 🗼",
  "format": "markdown",  // or "plain"
  "style": "normal"  // or "heading", "subheading", "caption"
}

Rendered As: - iMessage: Plain text with emoji - Telegram: Markdown-formatted message

2. Quick Reply

Purpose: Inline button choices (like iMessage tapbacks but custom text)

Schema:

{
  "type": "quick_reply",
  "prompt": "Want to add teamLab Museum?",
  "options": [
    {"id": "yes", "label": "Yes 👍", "emoji": "👍"},
    {"id": "no", "label": "No thanks", "emoji": "👎"},
    {"id": "maybe", "label": "Maybe", "emoji": "🤔"}
  ]
}

Rendered As: - iMessage: Text prompt + "Reply YES, NO, or MAYBE" (text-based) - Telegram: Inline keyboard buttons

Interaction: - User sends "yes" or taps button → Event emitted

3. Poll

Purpose: Multi-option voting with live results

Schema:

{
  "type": "poll",
  "question": "Which hotel?",
  "options": [
    {"id": "hotel_a", "label": "Hotel A - ¥20k/night", "votes": 2},
    {"id": "hotel_b", "label": "Hotel B - ¥15k/night", "votes": 3},
    {"id": "hotel_c", "label": "Hotel C - ¥30k/night", "votes": 1}
  ],
  "voted_by": {"+15551234567": "hotel_b", "+15559876543": "hotel_b"},
  "allow_multiple": false,
  "show_results": "always"  // or "after_vote"
}

Rendered As:

🗳️ Which hotel?
1. Hotel A - ¥20k/night  [👤👤] (2)
2. Hotel B - ¥15k/night  [👤👤👤] (3) ← winning
3. Hotel C - ¥30k/night  [👤] (1)

Reply with 1, 2, or 3 to vote

Interaction: - User sends "2" → Vote recorded, results updated

4. Rich Card

Purpose: Visual item with image, title, description, buttons

Schema:

{
  "type": "card",
  "image_url": "https://example.com/teamlab.jpg",
  "title": "teamLab Borderless",
  "subtitle": "Digital art museum in Odaiba",
  "description": "Immersive installation with 50+ interactive artworks",
  "footer": "¥3,200 per person | 2-3 hours",
  "actions": [
    {"id": "vote_yes", "label": "Add to plan", "style": "primary"},
    {"id": "vote_no", "label": "Skip", "style": "secondary"}
  ]
}

Rendered As: - iMessage: Image + text + "Reply ADD or SKIP" - Telegram: Native card with inline buttons

5. Table/List

Purpose: Structured data display (schedule, leaderboard, expense breakdown)

Schema:

{
  "type": "table",
  "title": "Trip Itinerary - Day 1",
  "columns": ["Time", "Activity", "Status"],
  "rows": [
    ["9:00 AM", "teamLab Museum", "✅ Confirmed (4 votes)"],
    ["12:30 PM", "Sushi lunch", "🤔 Pending (2 votes)"],
    ["3:00 PM", "Shibuya shopping", "✅ Confirmed (3 votes)"]
  ],
  "footer": "3 activities planned"
}

Rendered As:

📅 Trip Itinerary - Day 1

9:00 AM  teamLab Museum         ✅ Confirmed (4 votes)
12:30 PM Sushi lunch            🤔 Pending (2 votes)
3:00 PM  Shibuya shopping       ✅ Confirmed (3 votes)

3 activities planned

Purpose: Scrollable images or cards (shopping finds, activity options)

Schema:

{
  "type": "gallery",
  "title": "Hotel Options",
  "items": [
    {
      "image_url": "https://example.com/hotel_a.jpg",
      "title": "Hotel A",
      "caption": "¥20k/night | Near Shibuya",
      "action_id": "view_hotel_a"
    },
    // ... more items
  ],
  "layout": "horizontal"  // or "grid"
}

Rendered As: - iMessage: Multiple images sent in sequence with captions - Telegram: Media group

7. Form Block

Purpose: Multi-question input (onboarding, preferences)

Schema:

{
  "type": "form",
  "title": "Trip Preferences",
  "fields": [
    {
      "id": "budget",
      "label": "Budget per person?",
      "type": "number",
      "placeholder": "e.g., 200000"
    },
    {
      "id": "pace",
      "label": "Trip pace?",
      "type": "select",
      "options": ["Relaxed", "Balanced", "Packed"]
    }
  ]
}

Rendered As:

📝 Trip Preferences

Q1: Budget per person?
(Reply with a number, e.g., 200000)

[User replies "150000"]

Q2: Trip pace?
1. Relaxed
2. Balanced
3. Packed
(Reply 1, 2, or 3)

Interaction: - Sequential Q&A, AI guides through form

8. Progress/Meter

Purpose: Visual progress indicator (fitness challenge, budget tracker)

Schema:

{
  "type": "progress",
  "label": "Trip Planning Progress",
  "current": 7,
  "total": 10,
  "unit": "tasks completed",
  "show_percentage": true
}

Rendered As:

📊 Trip Planning Progress
7/10 tasks completed (70%)
[████████░░]

9. Alert/Notification

Purpose: High-priority message (price drop, deadline warning)

Schema:

{
  "type": "alert",
  "severity": "warning",  // info, warning, error, success
  "title": "⚠️ Cancellation Deadline",
  "message": "Hotel cancellation must be done by 6 PM today to avoid fees (¥8,000)",
  "actions": [
    {"id": "cancel_now", "label": "Cancel Reservation"},
    {"id": "keep", "label": "Keep It"}
  ]
}

Rendered As:

⚠️ Cancellation Deadline
Hotel cancellation must be done by 6 PM today to avoid fees (¥8,000)

Reply CANCEL or KEEP

Purpose: URL preview with metadata (flight booking, restaurant link)

Schema:

{
  "type": "share_link",
  "url": "https://www.google.com/flights/...",
  "title": "Flight: SFO → HND",
  "description": "April 3, 2026 | ANA 7 | $850",
  "image_url": "https://example.com/flight_preview.jpg",
  "action": "Open in Browser"
}

Rendered As: - iMessage: Rich link preview (if supported) - Telegram: Native link card


Alternatives Considered

Option 1: Custom Web Views (iMessage Apps)

Pros: - Full UI flexibility - Could build rich interactive UIs

Cons: - ❌ Requires users to install iMessage app extension - ❌ Breaks "zero friction" principle - ❌ Doesn't work on all devices - ❌ Not available in Telegram

Verdict: ❌ Violates constitution principle #1 (messaging-first)

Pros: - Flexible UI - Works across platforms

Cons: - ❌ Takes user out of iMessage (context switch) - ❌ Requires mobile-responsive design - ❌ Worse UX than in-chat interaction - ❌ Friction for quick interactions

Verdict: ❌ Too much friction, breaks conversation flow

Option 3: AI-Generated Natural Language Only (No Blocks)

Pros: - Pure text, very chat-native - No UI components needed

Cons: - ❌ Ambiguous for structured data (leaderboards, polls) - ❌ Hard to show live updates - ❌ No visual hierarchy

Verdict: ❌ Too limiting for rich interactions

Option 4: Standardized UI Blocks (CHOSEN)

Pros: - ✅ Works in all messaging platforms - ✅ Consistent, learnable UX - ✅ Accessible (text-based fallbacks) - ✅ Developer-friendly (JSON schemas) - ✅ Extensible (can add blocks later) - ✅ Composable (blocks can nest)

Cons: - ⚠️ Limited to 10 component types - ⚠️ Can't do everything a native app could

Mitigation: - 10 blocks cover 90% of use cases - Can add more blocks in future if needed - Focus on conversational interactions, not pixel-perfect UIs

Verdict:Best balance of flexibility and simplicity


Implementation Strategy

Block Renderer Architecture

# Base renderer interface
class BlockRenderer(ABC):
    @abstractmethod
    def render(self, block: Dict, platform: str) -> str:
        """Render block to platform-specific format"""
        pass

# Platform-specific renderers
class iMessageRenderer(BlockRenderer):
    def render(self, block: Dict, platform: str) -> str:
        if block["type"] == "poll":
            return self._render_poll_imessage(block)
        elif block["type"] == "card":
            return self._render_card_imessage(block)
        # ... etc

class TelegramRenderer(BlockRenderer):
    def render(self, block: Dict, platform: str) -> TelegramMessage:
        if block["type"] == "poll":
            return self._render_poll_telegram(block)
        # ... etc (uses Telegram Bot API native components)

Block Interaction Handler

class BlockInteractionHandler:
    async def handle_interaction(
        self,
        block_id: str,
        user_id: str,
        action: str,
        value: Any,
        context: Dict
    ) -> MiniAppEvent:
        """
        User interacted with a block (voted, clicked button, etc.)
        Convert to mini-app event
        """
        event = MiniAppEvent(
            action=f"block_{action}",
            payload={
                "block_id": block_id,
                "value": value,
                "block_type": context["block_type"]
            },
            user_id=user_id,
            room_id=context["room_id"],
            app_id=context["app_id"]
        )

        # Emit to event store
        await event_store.append_event(event)

        return event

Usage in Mini-Apps

Mini-apps declare which blocks they use:

# Trip Planner workflow
trip_planner_workflow = Workflow(
    id="trip_planner",
    ui_blocks_required=["poll", "card", "table", "gallery", "form"],
    nodes=[
        WorkflowNode(
            id="show_hotel_poll",
            type="render_block",
            config={
                "block": {
                    "type": "poll",
                    "question": "Which hotel?",
                    "options": "{{$node.fetch_hotels.results}}",
                    "room_id": "{{$context.room_id}}"
                }
            }
        )
    ]
)

Validation

Success Criteria

  1. Coverage: 90% of mini-app UX needs met by 10 blocks
  2. Performance: Block rendering <50ms
  3. Accessibility: All blocks have text fallbacks for screen readers
  4. Developer Velocity: New mini-app built in 2-3 days using blocks

User Testing

  • Build 3 mini-apps (Trip Planner, Fitness Challenge, Shopping)
  • Test with 20 users across iMessage and Telegram
  • Measure:
  • Task completion rate
  • Time to complete actions
  • Subjective satisfaction (1-5 scale)

Hypothesis: Users find block-based UI intuitive and prefer it to leaving chat.


Trade-offs

Pros

  1. Platform Portability: Works in iMessage, Telegram, future platforms
  2. Consistency: Users learn patterns once, apply everywhere
  3. Accessibility: Text-based rendering supports screen readers
  4. Developer Efficiency: Pre-built components, no custom UI code
  5. Versioning: Can enhance blocks without breaking mini-apps

Cons

  1. Constraints: Can't build everything (e.g., rich animations, complex layouts)
  2. Learning Curve: Developers must understand block system
  3. Platform Differences: iMessage vs Telegram render differently

Mitigation: - Document blocks extensively with examples - Provide templates for common patterns - Accept constraint as feature (forces simplicity)


Future Extensions

Potential New Blocks (Phase 2+)

  • Map Block: Embed location/route visualization
  • Calendar Block: Interactive date picker
  • Chart Block: Bar/line charts for analytics
  • Audio Block: Voice message or podcast clip
  • Video Block: Short video previews

Process for Adding Blocks: 1. Validate demand (3+ mini-apps need it) 2. Design JSON schema 3. Implement renderers for all platforms 4. Write docs + examples 5. Ship in minor version


References


Revision History

  • 2025-11-10: Initial draft