Technical Specification
← Back to Application

1. Overview

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.

1.1 Goals

1.2 Non-Goals (Current Version)

2. Architecture

2.1 System Diagram

┌───────────────────────────────────────────────────────────────────────────────┐ │ Browser Client │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ TimeSyncClient (sync-client.js) │ │ │ │ - WebSocket connection management │ │ │ │ - Heartbeat transmission (500ms interval) │ │ │ │ - RTT calculation via performance.now() │ │ │ │ - Clock offset estimation │ │ │ │ - Message submission with monotonic timestamps │ │ │ │ - Video recording (MediaRecorder API) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────────┘ │ WebSocket (wss://) │ HTTP POST (video) ▼ ┌───────────────────────────────────────────────────────────────────────────────┐ │ Cloudflare Edge Network │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Worker (src/index.ts) │ │ │ │ - Request routing │ │ │ │ - Static asset serving (public/) │ │ │ │ - Durable Object stub management │ │ │ │ - Two-phase video submission (claim + upload) │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │ │ TimeSyncDurableObject │ │ R2 Bucket (VIDEO_BUCKET) │ │ │ │ - WebSocket sessions │ │ - Video blob storage │ │ │ │ - WCT generation │ │ - wallflower-videos │ │ │ │ - Text submission storage │ │ - Max 50MB per video │ │ │ │ - Video metadata storage │ └──────────────────────────┘ │ │ └──────────────────────────┘ │ └───────────────────────────────────────────────────────────────────────────────┘

2.2 Component Summary

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

3. Protocol Specification

3.1 Transport Layer

3.2 Message Types

All messages are JSON-encoded UTF-8 strings.

3.2.1 Client → Server

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
}

3.2.2 Server → Client

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
}

3.3 RTT Calculation

RTT = T_receive - T_send

Where:
  T_send    = performance.now() at sync_request transmission
  T_receive = performance.now() at sync_response receipt

3.4 Clock Offset Estimation

Offset = server_wct - Date.now()

Estimated Server Time = Date.now() + Offset

4. Data Models

4.1 SubmissionRecord

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.

4.2 Storage Keys

Key Type Description
{uuid} SubmissionRecord Individual submission (text or video)
submission_index string[] Ordered list of submission IDs (max 1000)

5. API Reference

5.1 WebSocket Endpoint

Establishes bidirectional communication for time synchronization.

5.2 REST Endpoints

GET /api/status

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
  }
}

GET /api/submissions

Returns recent submissions.

Query Parameters:

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
}

5.3 Two-Phase Video Submission

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.

┌────────────────────────────────────────────────────────────────────┐ │ TWO-PHASE SUBMISSION │ │ │ │ Phase 1: CLAIM (instant) Phase 2: UPLOAD (slow) │ │ ┌───────────────────────┐ ┌─────────────────────────────┐ │ │ │ POST /api/claim-submission │ │ PUT /api/upload/{submissionId} │ │ │ │ - Lock clientWCT (auth) │ │ - Upload video blob │ │ │ │ - Create pending record │ │ - Store in R2 │ │ │ │ - Return submissionId │ ───▶ │ - Mark record complete │ │ │ └───────────────────────┘ └─────────────────────────────┘ │ │ │ │ Why: User presses Submit at T=0. Upload takes 30 seconds. │ │ WCT should be T=0, not T=30. │ └────────────────────────────────────────────────────────────────────┘

POST /api/claim-submission

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"
}

PUT /api/upload/{submissionId}

Phase 2: Uploads the video blob for a previously claimed submission.

Request:

Response:

{
  "success": true,
  "submissionId": "550e8400-e29b-41d4-a716-446655440000",
  "playbackUrl": "/video/550e8400-e29b-41d4-a716-446655440000.webm",
  "size": 1048576
}

GET /video/{objectKey}

Streams video content from R2 storage.

Response:

6. Client SDK

6.1 TimeSyncClient Class

class 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;
}

6.2 Usage Example

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}`);

7. Deployment

7.1 Prerequisites

7.2 Configuration

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

7.3 CI/CD Pipeline

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:

  1. Checkout repository
  2. Install dependencies (npm ci)
  3. Type check (tsc --noEmit)
  4. Deploy (wrangler deploy)

7.4 Commands

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

8. TAMS Integration

Wallflower implements the TAMS (Time-Addressable Media Store) Source/Flow architecture, serving as the TAMS Ingestion Gateway for fan-generated content.

8.1 TAMS Source

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
}

8.2 TAMS Flow Types

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

8.3 Game Book Reference Tuple

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;
}

8.4 API Endpoints

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)

9. WPL Provenance Ledger

The Wallflower Provenance Ledger (WPL) provides cryptographic proof of content authenticity through SHA-256 hashing of submission metadata and content.

9.1 WPL Transaction Structure

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;
  };
}

9.2 Hash Generation Process

  1. Collect all provenance metadata into payload object
  2. Serialize to canonical JSON (sorted keys)
  3. Compute SHA-256 hash of UTF-8 encoded string
  4. Store transaction with link to previous hash (chain)

9.3 Content Hash

Video content is hashed during upload (Phase 2) using SHA-256. The hash is stored in:

9.4 WPL API

GET /api/wpl/transactions

Returns recent WPL transactions with chain head.

{
  "transactions": [...],
  "count": 42,
  "chainHead": "a1b2c3d4..."
}

10. C2PA Content Credentials

Wallflower prepares C2PA (Content Credentials) manifest data for each submission. Actual cryptographic signing occurs at export time via external signing service.

10.1 C2PA Manifest Preparation

interface C2PAManifestPrep {
  claimGenerator: "Wallflower/2.0";
  title?: string;
  assertions: {
    creativeWork?: {
      author?: string;
      dateCreated: string;
    };
    actions: C2PAAction[];
  };
  ingredients: C2PAIngredient[];
}

10.2 Action Assertions

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"
  }
}

10.3 Provenance Status

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

10.4 Integration with WPL

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.

11. User Identity System

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.

11.1 How It Works

  1. First Visit: User is prompted to enter their display name via a modal dialog
  2. Cookie Storage: Username is stored in wallflower_username cookie (365-day expiry)
  3. Submission: Username is sent with every submission (text and video)
  4. Provenance: Username is stored in SubmissionRecord and included in WPL transaction hash
  5. Switch User: Users can change identity via "Switch User" button, which clears the cookie

11.2 What It Proves

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

11.3 Cookie Specification

Cookie Name: wallflower_username
Value: URL-encoded username (max 50 chars)
Expiry: 365 days
Path: /
SameSite: Lax

11.4 Limitations

For stronger identity guarantees, integrate with OAuth providers (Google, GitHub) or implement email verification in a future version.

12. Performance Characteristics

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

13. Security Considerations

12.1 Current Implementation

12.2 Recommended Enhancements (Future)

14. Future Roadmap

13.1 WebTransport Migration

When Cloudflare Workers fully support WebTransport:

  1. Add WebTransport handler to Durable Object
  2. Implement unidirectional streams for sync requests
  3. Add bidirectional streams for submissions
  4. Client auto-detection with WebSocket fallback

13.2 Enhanced Time Sync

13.3 Scalability

15. File Structure

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

16. References