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¶
- Platform Support: Must work in iMessage (primary) and Telegram
- Zero Friction: No app downloads or web views
- Conversational: Feels like chat, not a separate app
- Consistent UX: All mini-apps have similar interaction patterns
- Developer Velocity: Easy to build new mini-apps
- 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
6. Gallery¶
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:
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
10. Share Link Card¶
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)
Option 2: Dynamic Web Pages (Links to Web UI)¶
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¶
- Coverage: 90% of mini-app UX needs met by 10 blocks
- Performance: Block rendering <50ms
- Accessibility: All blocks have text fallbacks for screen readers
- 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¶
- Platform Portability: Works in iMessage, Telegram, future platforms
- Consistency: Users learn patterns once, apply everywhere
- Accessibility: Text-based rendering supports screen readers
- Developer Efficiency: Pre-built components, no custom UI code
- Versioning: Can enhance blocks without breaking mini-apps
Cons¶
- Constraints: Can't build everything (e.g., rich animations, complex layouts)
- Learning Curve: Developers must understand block system
- 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¶
- Telegram Bot API: Inline Keyboards
- iMessage: Rich Link Previews
- Wabi: Mini-App UI Examples
Revision History¶
- 2025-11-10: Initial draft