Internal Services
Backend service architecture and implementation details for Sketch2Screen.
Overview
The backend consists of three main service components:
- Claude Client - AI-powered sketch-to-code generation
- Collaboration Server - Real-time session and state management
- WebSocket Consumer - WebSocket message handling
Claude Client Service
Module: backend/sketch_api/services/claudeClient.py
The Claude Client service handles all interactions with the Anthropic Claude API for AI-powered sketch-to-code generation.
Functions
image_to_html_css()
Purpose: Main entry point for converting sketch images to HTML/CSS using Claude AI.
Signature:
async def image_to_html_css(
image_bytes: bytes,
media_type: str = "image/png",
prompt: Optional[str] = None
) -> str
Note: This is an async function and must be awaited.
Parameters:
image_bytes(bytes) - Raw binary data of the image filemedia_type(str, optional) - MIME type of the image (default:"image/png")prompt(str | None, optional) - Custom user instruction for code generation (default:None)
Returns:
str- Generated HTML and CSS code
Raises:
RuntimeError- If API key is missing or Claude returns no textFileNotFoundError- If API key file path is invalid
Example:
from sketch_api.services.claudeClient import image_to_html_css
import asyncio
# Read image file
with open('sketch.png', 'rb') as f:
image_bytes = f.read()
# Generate HTML/CSS (must await)
html = await image_to_html_css(
image_bytes=image_bytes,
media_type="image/png",
prompt="Make this design responsive"
)
print(html)
How It Works:
- Base64 Encoding - Converts image bytes to base64 string
- Prompt Construction - Combines system message with user instruction
- API Call - Sends image + prompt to Claude via Anthropic SDK
- Response Parsing - Extracts text content from Claude response
- Validation - Ensures response is not empty
System Prompt:
You are an expert frontend developer specializing in converting UI sketches into
modern, high-fidelity, pixel-perfect, production-ready HTML with Tailwind CSS. Your code is clean, semantic,
accessible, and follows modern web development best practices.
Default User Instruction:
Convert the provided UI sketch into complete, functional HTML with Tailwind CSS styling.
CRITICAL OUTPUT REQUIREMENTS:
- Ignore the outer bounding box of sketch as it is for user to assume as a viewport.
- Return ONLY raw HTML code - no markdown fences, no explanations, no preamble
- This renders in an iframe, so include complete document structure with <!DOCTYPE html>
- Set body to full viewport dimensions: class="h-screen w-screen" (100vh height, 100vw width)
- Do not add padding to outside of the borders, only use spacing within the layout itself
- Ensure the color scheme matches the sketch.
CODE QUALITY STANDARDS:
- Design generated should be of extremely high fidelity.
- Use semantic HTML5 elements (<header>, <nav>, <main>, <section>, <article>, <footer>)
- Include all necessary JavaScript for interactivity (e.g., dropdowns, modals, tabs)
- Minimize unnecessary wrapper divs - keep markup lean
- Ensure all elements have explicit dimensions or content to render properly
- Make layouts responsive using Tailwind's responsive prefixes (sm:, md:, lg:)
- The design should look production-ready, not like a prototype
DESIGN FIDELITY:
- Match the sketch's layout and color scheme but make it modern, clean, and visually appealing
- If elements are labeled with HTML tag names (e.g., "button", "input", "img"), use those exact tags
- Preserve all text content visible in the sketch
- For images in the design: Use placeholder divs or external placeholder images
- Represent icons using emoji, Unicode symbols, or labeled boxes
Begin your response with <!DOCTYPE html> and nothing else.
Configuration:
- Model:
claude-haiku-4-5-20251001(configurable viaCLAUDE_MODELsetting) - Max Tokens: 15000
- API Key: Loaded from file or environment variable
_client()
Purpose: Internal helper to create authenticated Anthropic client.
Signature:
def _client() -> AsyncAnthropic
Returns:
AsyncAnthropic- Authenticated async API client instance
Raises:
RuntimeError- If API key is not configured
API Key Loading Strategy:
- Try to load from file specified in
CLAUDE_API_KEYsetting - If file fails, try
ANTHROPIC_API_KEYenvironment variable - If both fail, raise
RuntimeError
Example:
client = _client()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1000,
messages=[...]
)
_load_anthropic_key_from_file()
Purpose: Internal helper to read API key from file.
Signature:
def _load_anthropic_key_from_file() -> str
Returns:
str- API key (whitespace trimmed)
Raises:
RuntimeError- IfCLAUDE_API_KEYsetting is not configuredRuntimeError- If API key file is emptyFileNotFoundError- If file path doesn't existPermissionError- If file cannot be read
Example Django Settings:
# settings.py
CLAUDE_API_KEY = "/path/to/api_key.txt"
_extract_text()
Purpose: Internal helper to extract text from Claude API response.
Signature:
def _extract_text(resp) -> str
Parameters:
resp(Message) - Anthropic API response object
Returns:
str- Concatenated text from all text-type content blocks
Example Response Structure:
# Claude response object
{
"content": [
{"type": "text", "text": "<div>HTML code here</div>"},
{"type": "text", "text": "<style>CSS here</style>"}
]
}
# Extracted result
"<div>HTML code here</div><style>CSS here</style>"
Collaboration Server
Module: backend/sketch_api/CollabServer.py
The Collaboration Server manages real-time collaboration sessions using the Singleton pattern.
Architecture
┌─────────────────────────────────────────────────┐
│ CollabServer (Singleton) │
├─────────────────────────────────────────────────┤
│ collabSessions: { │
│ "12345": CollabSession { │
│ members: ["user1", "user2", "user3"] │
│ sketches: [ │
│ Sketch { ID, name, sceneData } │
│ ] │
│ } │
│ } │
└─────────────────────────────────────────────────┘
Functions
applyDiff(base, diff)
Purpose: Apply a differential update to a base scene object, merging changes recursively.
Signature:
def applyDiff(base, diff)
Parameters:
base(dict | list | any) - The base scene data to apply changes todiff(dict | list | any) - The differential changes to apply
Returns:
dict | list | any- Merged result with diff applied to base
How It Works:
- Converts lists to dicts with string indices for uniform processing
- Recursively merges diff into base
- Preserves base values not mentioned in diff
- Adds new keys from diff not in base
- Converts back to list if original was list
Example:
base = {"elements": [{"id": "1", "x": 10}], "appState": {"zoom": 1}}
diff = {"elements": {"0": {"x": 20}}} # Update x coordinate of first element
result = applyDiff(base, diff)
# Result: {"elements": [{"id": "1", "x": 20}], "appState": {"zoom": 1}}
Use Case:
- Efficiently synchronize scene changes without sending entire scene
- Reduces network payload for collaboration updates
- Prevents mid-stroke conflicts by only sending deltas
Classes
Collaborator
Purpose: Represents a user participating in a collaboration session.
Constructor:
def __init__(self, userID, username, channelName):
self.userID = userID
self.username = username
self.channelName = channelName
self.pointer = None
self.currentPage = None
Attributes:
userID(str) - Client-generated unique identifierusername(str) - Display name for this userchannelName(str) - Django Channels WebSocket identifier (internal)pointer(dict | None) - Current cursor position{x, y}orNonecurrentPage(str | None) - ID of the page user is currently viewing
Example:
collaborator = Collaborator(
userID="user-1234567890-abc123",
username="Alice",
channelName="specific.channel.xyz789"
)
collaborator.pointer = {"x": 150, "y": 200}
collaborator.currentPage = "page-1"
SingletonMeta
Purpose: Metaclass that implements the Singleton design pattern.
Usage:
class MyClass(metaclass=SingletonMeta):
pass
instance1 = MyClass()
instance2 = MyClass()
assert instance1 is instance2 # True - same instance
Implementation:
class SingletonMeta(type):
_instance = None
def __call__(cls, *args, **kwargs):
if cls._instance == None:
cls._instance = super(SingletonMeta, cls).__call__(*args, **kwargs)
return cls._instance
CollabSession
Purpose: Represents a single collaboration session.
Attributes:
members(list[str]) - List of connected user channel namessketches(list[Sketch]) - List of sketch objects in this session
Example:
session = CollabSession()
session.members = ["specific.channel.abc123", "specific.channel.def456"]
session.sketches = [
Sketch("Homepage", "page-1", {...}),
Sketch("About", "page-2", {...})
]
Sketch
Purpose: Represents a single sketch/page within a collaboration session.
Constructor:
def __init__(self, name, ID, sceneData):
self.name = name
self.ID = ID
self.sceneData = sceneData
Attributes:
name(str) - Display name of the sketch pageID(str | int) - Unique identifier for the sketchsceneData(dict) - Excalidraw scene data (elements, appState, etc.)
Example:
sketch = Sketch(
name="Contact Page",
ID="page-3",
sceneData={
"elements": [...],
"appState": {...}
}
)
CollabServer
Purpose: Singleton server managing all collaboration sessions and message routing.
Metaclass: SingletonMeta
Attributes:
collabSessions(dict) - MapscollabID→CollabSession
Methods:
onNewConnection(userID, collabID)
Purpose: Handle new user connecting to a collaboration session.
Parameters:
userID(str) - Channel name of the connecting usercollabID(str) - Collaboration session ID
Behavior:
- Creates new session if it doesn't exist
- Adds user to session members list
- Sends all existing sketches to the new user
Example:
server = CollabServer()
server.onNewConnection("specific.channel.abc123", "12345")
onSceneUpdate(userID, collabID, sketchID, sceneData)
Purpose: Handle scene update from a user.
Parameters:
userID(str) - Channel name of the user sending updatecollabID(str) - Collaboration session IDsketchID(str) - ID of the sketch being updatedsceneData(dict) - New scene data
Behavior:
- Validates sketch exists in session (discards if not found)
- Updates stored scene data
- Broadcasts update to all members except sender
Example:
server.onSceneUpdate(
userID="specific.channel.abc123",
collabID="12345",
sketchID="page-1",
sceneData={"elements": [...], "appState": {...}}
)
onPageUpdate(userID, collabID, sketchID, pageName)
Purpose: Handle page create, rename, or delete operation.
Parameters:
userID(str) - Channel name of the user sending updatecollabID(str) - Collaboration session IDsketchID(str) - ID of the sketch pagepageName(str | None) - New name, orNoneto delete
Behavior:
- If sketch not found: Creates new sketch with empty scene data
- If
pageNameisNone: Deletes sketch from session - Otherwise: Updates sketch name
Example:
# Create/rename page
server.onPageUpdate("user1", "12345", "page-2", "About Us")
# Delete page
server.onPageUpdate("user1", "12345", "page-2", None)
onConnectionEnd(userID, collabID)
Purpose: Handle user disconnecting from session.
Parameters:
userID(str) - Channel name of the disconnecting usercollabID(str) - Collaboration session ID
Behavior:
- Removes user from session members list
- If session has no remaining members, deletes the entire session
Example:
server.onConnectionEnd("specific.channel.abc123", "12345")
sendSceneUpdate(userID, sketchID, sceneData)
Purpose: Send scene update message to a specific user via Django Channels.
Parameters:
userID(str) - Target user channel namesketchID(str) - ID of the updated sketchsceneData(dict) - Updated scene data
Example:
server.sendSceneUpdate(
userID="specific.channel.def456",
sketchID="page-1",
sceneData={"elements": [...]}
)
Internally uses:
async_to_sync(get_channel_layer().send)(userID, {
"type": "scene.update",
"sketchID": sketchID,
"sketchData": sceneData
})
sendPageUpdate(userID, sketchID, pageName)
Purpose: Send page update message to a specific user.
Parameters:
userID(str) - Target user channel namesketchID(str) - ID of the updated sketchpageName(str | None) - New name orNonefor deletion
Example:
server.sendPageUpdate(
userID="specific.channel.def456",
sketchID="page-2",
pageName="Contact"
)
WebSocket Consumer
Module: backend/sketch_api/consumers.py
Handles WebSocket connections and routes messages to CollabServer.
WebSocket Endpoint: ws://localhost:8000/ws/collab/{collabID}/
Note: This is an experimental feature and may have bugs.
Class: SketchConsumer
Inherits: channels.generic.websocket.WebsocketConsumer
Attributes:
server(CollabServer) - Singleton instance (class variable)collabID(str) - Collaboration ID for this connection (instance variable)
WebSocket Protocol
Connection Lifecycle:
- Client connects → Server calls
onNewConnection(channelName, collabID) - Server sends existing state → Client receives all current pages via
page_updateandscene_update, plus existing collaborators viacollaborator_join - Client sends
collaborator_join→ Server broadcasts new user to others - Client sends updates → Server broadcasts scene diffs to other members (excluding sender)
- Client disconnects → Server calls
onConnectionEnd(channelName, collabID), sendscollaborator_leaveto others, and cleans up if last member
Message Types (Client → Server):
scene_update- Scene drawing changes (with diff for efficiency)page_update- Page create/rename/delete (null name = delete)collaborator_join- User joined with userID and usernamecollaborator_pointer- Cursor position update (throttled to 50ms, includes pageID)
Message Types (Server → Client):
scene_update- Another user's drawing changespage_update- Another user's page operationscollaborator_join- New user joined the sessioncollaborator_leave- User disconnectedcollaborator_pointer- Another user's cursor moved (filtered by pageID)
Key Methods
connect()
Extracts collabID from URL route, calls server.onNewConnection(), accepts WebSocket connection.
disconnect(close_code)
Calls server.onConnectionEnd() to clean up session.
receive(text_data)
Parses JSON message, routes based on action field to:
scene_update→onSceneUpdate()page_update→onPageUpdate()collaborator_join→onCollaboratorJoin()collaborator_pointer→onCollaboratorPointer()
scene_update(event), page_update(event), collaborator_join(event), collaborator_leave(event), collaborator_pointer(event)
Django Channels handlers that serialize events and send to WebSocket client.
Example Message Flow:
# Client sends
{"action": "scene_update", "sketchID": "page-1", "sketchData": {...}}
# Server broadcasts to others
{"action": "scene_update", "sketchID": "page-1", "sketchData": {...}}
Configuration
Django Settings
Required:
# settings.py
# Claude API key file path or use ANTHROPIC_API_KEY env var
CLAUDE_API_KEY = "/path/to/api_key.txt"
# Optional: Claude model to use (default shown)
CLAUDE_MODEL = "claude-sonnet-4-20250514"
# Channel layers for WebSocket
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
Environment Variables:
# Alternative to CLAUDE_API_KEY setting
export ANTHROPIC_API_KEY="sk-ant-..."
# Set to True for production mode
export PROD=True
Performance Considerations
Claude Client
- Synchronous API calls - Blocks request thread during generation
- Average response time: 3-10 seconds per image
- Token limit: 2000 tokens per response
- Rate limits: Depends on Anthropic API plan
Optimization Ideas:
- Use async/await for non-blocking requests
- Implement request queuing for high traffic
- Cache common sketch patterns
Collaboration Server
- In-memory storage - Fast but ephemeral
- No persistence - All data lost on restart
- Single-instance limitation - InMemoryChannelLayer doesn't scale horizontally
Scaling Recommendations:
- Use Redis channel layer for multi-server deployments
- Implement database persistence for sketches
- Add session expiration and cleanup
WebSocket Consumer
- One consumer instance per connection
- Message broadcasting - O(n) where n = number of session members
- No message buffering - Messages sent immediately