Wallflower is The Synchronized Fan Contribution Engine - a real-time time synchronization and fan media submission service built on Cloudflare Workers and Durable Objects. It provides authoritative Wall-Clock Timecode (WCT) stamping for fan-generated video and text contributions with sub-second precision. All fans share a common time reference for true synchronized collaboration.
| Component | Location | Responsibility |
|---|---|---|
| Worker Entry Point | src/index.ts |
HTTP routing, DO stub creation, two-phase submission |
| Durable Object | src/TimeSyncDurableObject.ts |
State management, WebSocket handling, metadata |
| R2 Bucket | wallflower-videos |
Video blob storage |
| Browser Client | public/sync-client.js |
Connection, RTT calculation, submission |
| UI - Submit | public/index.html |
Contribution interface, video recording, status display |
| UI - Log | public/log.html |
View all contributions from all users |
| UI - Provenance | public/provenance.html |
Provenance Dashboard (TAMS, WPL, Game Book) |
| UI - Overview | public/overview.html |
Application overview and documentation |
| UI - Sample | public/sample.html |
Sample application with Super Bowl LX simulation |
| UI - Admin | public/admin.html |
Password-protected admin panel |
/connect/syncAll messages are JSON-encoded UTF-8 strings.
Sync Request
{
type: "sync_request",
client_monotonic_ts: number // performance.now() value
}
User Submission
{
type: "user_submission",
client_monotonic_ts: number, // performance.now() value
clientWCT: number, // Date.now() at submit (authoritative)
message: string // User-provided content
}
Sync Response
{
type: "sync_response",
server_wct: number, // Date.now() - authoritative timestamp
client_monotonic_ts: number // Echo of client timestamp for RTT calc
}
Submission Acknowledgment
{
type: "submission_ack",
id: string, // UUID of stored record
server_wct: number, // Authoritative timestamp
success: boolean
}
Error
{
type: "error",
message: string,
server_wct: number
}
RTT = T_receive - T_send
Where:
T_send = performance.now() at sync_request transmission
T_receive = performance.now() at sync_response receipt
Offset = server_wct - Date.now()
Estimated Server Time = Date.now() + Offset
Unified record for both text and video submissions, stored in Durable Object persistent storage.
interface SubmissionRecord {
id: string; // UUID v4
type: 'text' | 'video'; // Submission type
status: 'pending' | 'complete'; // Two-phase status
clientIp: string; // CF-Connecting-IP header
serverWCT: number; // Server timestamp at receipt (ms since epoch)
clientWCT?: number; // Client timestamp at submit (authoritative)
clientMonotonicTs?: number; // Client's performance.now() value
createdAt: string; // ISO 8601 timestamp (from clientWCT)
username?: string; // Contributor display name
// Text submissions
clientMessage?: string; // User-submitted text content
// Video submissions
contentType?: string; // MIME type (video/webm, video/mp4)
expectedSize?: number; // Claimed size in bytes
objectKey?: string; // R2 object key after upload
actualSize?: number; // Actual uploaded size
completedAt?: string; // ISO 8601 timestamp of upload completion
}
Authoritative Time: clientWCT is the authoritative timestamp representing the user's moment of intent (when they clicked Submit). serverWCT is retained for verification. The delta between them reveals network latency.
| Key | Type | Description |
|---|---|---|
{uuid} |
SubmissionRecord | Individual submission (text or video) |
submission_index |
string[] | Ordered list of submission IDs (max 1000) |
wss://{host}/connect/syncEstablishes bidirectional communication for time synchronization.
Returns current Durable Object status.
Response:
{
"status": "online",
"server_wct": 1734107200000,
"active_sessions": 3,
"total_submissions": 42,
"recent_stats": {
"pending": 2,
"complete": 40,
"videos": 5,
"texts": 35
}
}
Returns recent submissions.
Query Parameters:
limit (optional): Max records to return (default: 50, max: 100)status (optional): Filter by status (pending, complete, or omit for all)Response:
{
"submissions": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"clientIp": "192.0.2.1",
"serverWCT": 1734107200000,
"clientMessage": "Hello, World!",
"createdAt": "2025-12-13T12:00:00.000Z"
}
],
"count": 1
}
Video submission uses a two-phase protocol to ensure the timestamp reflects the moment of creation (when user clicks Submit), not when the upload completes.
Phase 1: Claims a submission timestamp immediately. The client's clientWCT is the authoritative "moment of creation."
Request:
{
"type": "video",
"clientMonotonicTs": 12345.67,
"clientWCT": 1734107200000,
"size": 1048576,
"contentType": "video/webm"
}
Response:
{
"success": true,
"submissionId": "550e8400-e29b-41d4-a716-446655440000",
"serverWCT": 1734107200050,
"clientWCT": 1734107200000,
"status": "pending",
"createdAt": "2025-12-13T12:00:00.000Z"
}
Phase 2: Uploads the video blob for a previously claimed submission.
Request:
video/webm, video/mp4, or video/quicktimeResponse:
{
"success": true,
"submissionId": "550e8400-e29b-41d4-a716-446655440000",
"playbackUrl": "/video/550e8400-e29b-41d4-a716-446655440000.webm",
"size": 1048576
}
Streams video content from R2 storage.
Response:
video/webm or video/mp4public, max-age=31536000class TimeSyncClient {
constructor(options: {
endpoint?: string; // Default: '/connect/sync'
heartbeatInterval?: number; // Default: 500 (ms)
onStatusChange?: (status: string) => void;
onRttUpdate?: (data: RttData) => void;
onSubmissionAck?: (ack: SubmissionAck) => void;
onError?: (error: Error) => void;
});
connect(): void;
disconnect(): void;
submitMessage(message: string): Promise<SubmissionAck>;
getEstimatedServerTime(): number | null;
// Properties
isConnected: boolean;
currentRtt: number | null;
averageRtt: number | null;
clockOffset: number | null;
}
const client = new TimeSyncClient({
onRttUpdate: (data) => {
console.log(`RTT: ${data.rtt.toFixed(2)}ms`);
console.log(`Offset: ${data.clockOffset}ms`);
}
});
client.connect();
// Submit a message
const ack = await client.submitMessage("User event");
console.log(`Stored with ID: ${ack.id} at WCT: ${ack.server_wct}`);
npm install -g wrangler)wrangler.jsonc:
{
"name": "wallflower",
"main": "src/index.ts",
"compatibility_date": "2025-12-13",
"durable_objects": {
"bindings": [
{
"name": "TIMESYNC",
"class_name": "TimeSyncDurableObject"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["TimeSyncDurableObject"]
}
],
"r2_buckets": [
{
"binding": "VIDEO_BUCKET",
"bucket_name": "wallflower-videos"
}
]
}
Note: The R2 bucket must be created via Cloudflare dashboard or CLI: wrangler r2 bucket create wallflower-videos
Automated deployment via GitHub Actions on push to main.
Required GitHub Secrets:
| Secret | Description |
|---|---|
CLOUDFLARE_ACCOUNT_ID |
Cloudflare account identifier |
CLOUDFLARE_API_TOKEN |
API token with "Edit Cloudflare Workers" permissions |
Pipeline Steps:
npm ci)tsc --noEmit)wrangler deploy)| Command | Description |
|---|---|
npm run dev |
Start local development server |
npm run deploy |
Deploy to Cloudflare |
npm run cf-typegen |
Regenerate TypeScript types |
npm test |
Run test suite |
Wallflower implements the TAMS (Time-Addressable Media Store) Source/Flow architecture, serving as the TAMS Ingestion Gateway for fan-generated content.
A Source represents an abstract piece of content and serves as the session-level anchor for all Flows.
interface TAMSSource {
id: string; // UUID v4 - immutable session anchor
createdAt: string; // ISO 8601
label?: string;
description?: string;
flows: TAMSFlow[]; // Collected Flows
sessionId: string; // Session identifier
clientIp: string; // Source verification
}
Flows are time-indexed streams of media or metadata, collected under Sources.
| Flow Type | Role | Purpose |
|---|---|---|
| FRP Media Flow | frp-media |
Fan Response Package video essence (stored in R2) |
| PPP Metadata Flow | ppp |
Participant Profile Package (fan identity/context) |
| Game Book Reference Flow | gamebook-ref |
Synchronization tuple linking fan reaction to broadcast event |
| C2PA Metadata Flow | c2pa |
Detached C2PA manifest storage |
The synchronization tuple correlates fan reactions with official broadcast events:
interface GameBookReferenceTuple {
officialGameFlowId: string; // UUID of broadcast feed
gameWCT: number; // WCT from Game Book data
frpMediaFlowId: string; // UUID of fan's video Flow
frpWCT: number; // WCT of fan's reaction start
eventType?: string; // e.g., "Touchdown", "First and 10"
eventDescription?: string;
}
| Endpoint | Method | Description |
|---|---|---|
/api/sources |
GET | List all TAMS Sources |
/api/source/{id} |
GET | Get specific Source with Flows |
/api/flow/{id} |
GET | Get specific Flow details |
/api/gamebook/markers |
GET/POST | List or add Game Book markers |
/api/reset |
POST | Clear all data (R2 + Durable Object storage) |
The Wallflower Provenance Ledger (WPL) provides cryptographic proof of content authenticity through SHA-256 hashing of submission metadata and content.
interface WPLTransaction {
sourceId: string; // TAMS Source anchor
transactionHash: string; // SHA-256 of canonical JSON payload
previousHash?: string; // Chain link to previous transaction
timestamp: number; // WCT when hash was generated
payload: {
sourceId: string;
flowIds: string[];
clientIp: string;
username?: string; // Contributor identity
serverWCT: number;
clientMonotonicTs?: number;
contentHash?: string; // SHA-256 of media essence
gameBookReference?: GameBookReferenceTuple;
};
}
Video content is hashed during upload (Phase 2) using SHA-256. The hash is stored in:
contentHash)contentHash field)GET /api/wpl/transactions
Returns recent WPL transactions with chain head.
{
"transactions": [...],
"count": 42,
"chainHead": "a1b2c3d4..."
}
Wallflower prepares C2PA (Content Credentials) manifest data for each submission. Actual cryptographic signing occurs at export time via external signing service.
interface C2PAManifestPrep {
claimGenerator: "Wallflower/2.0";
title?: string;
assertions: {
creativeWork?: {
author?: string;
dateCreated: string;
};
actions: C2PAAction[];
};
ingredients: C2PAIngredient[];
}
Wallflower records the c2pa.created action at submission time:
{
action: "c2pa.created",
when: "2025-12-14T12:00:00.000Z",
softwareAgent: "Wallflower SFCE",
parameters: {
sourceId: "...",
serverWCT: 1734181200000,
clientIp: "192.0.2.1"
}
}
The c2paProvenance field tracks manifest status:
| Value | Meaning |
|---|---|
none |
No C2PA manifest (default after creation) |
embedded |
Manifest embedded in exported media file |
detached |
Manifest stored as separate data Flow |
C2PA and WPL serve complementary roles:
The WPL transaction hash can be included as a C2PA assertion, linking the two systems.
Note: C2PA signing requires a valid signing certificate. For production use, obtain certificates from CAI members or use IPTC Verified News Publisher certificates for news organizations.
Wallflower includes a cookie-based user identity system that adds contributor attribution to the provenance chain. This allows submissions to be associated with a named contributor rather than just an IP address.
wallflower_username cookie (365-day expiry)| Claim | Strength | Notes |
|---|---|---|
| Who submitted | Weak | Self-declared name, no verification |
| When submitted | Strong | Server-authoritative WCT timestamp |
| Content integrity | Strong | SHA-256 content hash |
| Submission order | Strong | WPL hash chain |
Cookie Name: wallflower_username
Value: URL-encoded username (max 50 chars)
Expiry: 365 days
Path: /
SameSite: Lax
For stronger identity guarantees, integrate with OAuth providers (Google, GitHub) or implement email verification in a future version.
| Metric | Value |
|---|---|
| Heartbeat Interval | 500ms |
| RTT History Window | 20 samples |
| Max Stored Submissions | 1000 (rolling) |
| Max Stored Video Metadata | 1000 (rolling) |
| Max Video Size | 50MB |
| Supported Video Formats | WebM, MP4, MOV |
| WebSocket Reconnect Attempts | 5 |
| Reconnect Base Delay | 1000ms |
When Cloudflare Workers fully support WebTransport:
wallflower/
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CD pipeline
├── src/
│ ├── index.ts # Worker entry point
│ └── TimeSyncDurableObject.ts # Durable Object class
├── public/
│ ├── index.html # Submit UI (text & video contributions)
│ ├── log.html # Contribution log (view all submissions)
│ ├── provenance.html # Provenance Dashboard
│ ├── overview.html # Application overview
│ ├── sample.html # Sample application (Super Bowl LX simulation)
│ ├── admin.html # Admin panel (password protected)
│ ├── faq.html # FAQ and TAMS comparison
│ ├── spec.html # Technical specification (this document)
│ ├── favicon.svg # Sunflower favicon
│ ├── sync-client.js # Browser client SDK
│ └── data/
│ └── superbowl-lx-gamebook.json # Sample Game Book data
├── test/
│ └── index.spec.ts # Test suite
├── wrangler.jsonc # Cloudflare configuration
├── tsconfig.json # TypeScript configuration
├── package.json # Dependencies
└── specification.md # Markdown specification