Skip to content
PRD: Ambient Co-presence

PRD: Ambient Co-Presence on Any Website

Overview

A lightweight co-presence system for the we were online browser extension that makes the internet feel inhabited without being intrusive. Unlike the full co-presence system on Wikipedia (live cursors, link patina, follow mode), this feature provides a subtle, ambient sense of other people on any website.
Two components:
Blurred Cursor Dot — see other extension users on the same page as a soft, defocused dot that sharpens into a real cursor as you move close to them
Fading Trail — when someone leaves a page, their last few seconds of cursor movement linger briefly as a warm, fading trail

Core Philosophy

“These moments of intimacy feel so special because they connect people who most likely would’ve never encountered each other otherwise. They feel a bit like a rift in reality that wasn’t planned.”
This feature should feel like catching a glimpse of someone in a park — not surveillance, not social media, just ambient awareness that you share a space. It should be impossible to find annoying — the absolute worst case is that someone doesn’t notice it at all.

Feature 1: Blurred Cursor Dot

Behavior

When another extension user is on the same page as you, their cursor appears as a soft, blurred, colored dot — not a full cursor. As your cursor moves closer to theirs, the dot gradually sharpens and gains definition, eventually revealing their full cursor (with any customization they’ve applied).

Visual Specification

The Dot (far away / default state):
Rendered as a radial gradient circle, ~20px diameter
Color: warm, semi-transparent (suggest rgba(255, 180, 120, 0.4) or derive from user’s identity hash for unique-per-person color)
Gaussian blur: filter: blur(8px)
Subtle ambient animation: very slow “breathing” pulse (opacity oscillates between 0.3–0.5 over ~4 seconds)
No pointer events — completely non-interactive at this distance
Proximity Reveal (as cursors approach each other):
Trigger distance: begin reveal at 200px between cursors
Full reveal at: 50px between cursors
Interpolation: linear or ease-out between blur states
At 200px: blur(8px), opacity 0.4, 20px diameter
At 125px: blur(5px), opacity 0.55, 18px diameter
At 50px: blur(0px), opacity 0.8, actual cursor size
Below 50px: full cursor visible with subtle warm glow around it
Transition timing:
Blur-to-sharp transition: 200ms ease-out (should feel organic, not snappy)
Position updates: aim for ~10fps cursor position broadcast (not 60fps — this is ambient, not collaborative editing). Smooth with CSS transitions on the receiving end.

Edge Cases

Multiple users on same page: Each gets their own uniquely colored dot. Cap visible dots at 5 (show nearest 5 if more are present). Display a subtle count indicator if >5 users present.
User on a very long/wide page: Only show dots for users within the current viewport ± 1 viewport height. Don’t render dots for users scrolled far away.
User idle: If a user’s cursor hasn’t moved for >60 seconds, fade their dot to 0 opacity over 10 seconds. Reappear on movement.
User’s own cursor: Never show. Obvious but worth stating.
Pages where it doesn’t make sense: Initially ship on ALL pages. Collect data on which domains users have the extension active on. We may add an allowlist/blocklist later but don’t prematurely optimize.
Performance: The dot is a single absolutely-positioned div with CSS blur. This should have negligible performance impact. If blur causes issues on older hardware, fall back to a simple semi-transparent circle.

Data Flow

User A moves cursor
→ Extension content script captures mousemove (throttled to 100ms intervals)
→ Sends to PartyKit room (room ID = hash of canonical URL)
→ PartyKit broadcasts to other connections in same room
→ User B's content script receives position
→ Renders/updates the blurred dot via CSS transform (no DOM reflow)
Room keying: Use normalized URL (strip query params, fragments, trailing slashes, normalize protocol) as the room identifier. Two users on https://example.com/page and http://example.com/page/ should be in the same room.
Connection lifecycle:
Extension opens a WebSocket to PartyKit when a page loads
Sends heartbeat every 30 seconds
Disconnects on page unload (triggers trail, see below)
Reconnects on visibility change (tab becomes active again)

Feature 2: Fading Trail

Behavior

When a user leaves a page (navigates away, closes tab), their last 3 seconds of cursor movement is rendered as a fading trail for remaining users on that page. The trail lingers for 8 seconds then fully fades. This creates a ghostly “someone was just here” feeling — you see the echo of their movement after they’ve gone.
We should also show trails from up to 14 days ago (prioritize sooner ones and limit to 5 distinct people max so it’s not overwhelming). They should be more faded the longer they are from the present

Visual Specification

Trail rendering:
Composed of cursor positions sampled over the last 3 seconds before departure (~30 points at 10fps)
Rendered as a polyline or series of small circles with decreasing opacity
Color: same as the user’s dot color, but slightly warmer/more saturated
Oldest points: opacity 0.1, size 3px
Newest points (where they “left”): opacity 0.5, size 6px
The entire trail fades out over 8 seconds: multiply all opacities by a global fade factor (1.0 → 0.0 over 8s, ease-in curve so it lingers then vanishes)
Trail style:
use the same trail rendering that we do in the portrait
On departure:
The user’s blurred dot transitions into the trail head (the dot becomes the last point of the trail)
The transition should feel like the dot “dissolved” into a trail

Data Flow

User A navigates away / closes tab
→ Content script fires beforeunload
→ Sends last 3 seconds of buffered cursor positions to PartyKit
→ PartyKit broadcasts "user_departed" event with trail data to remaining connections
→ Remaining users' content scripts render the fading trail
→ Trail auto-removes after 8 seconds
Important: The cursor position buffer (last 3 seconds) is maintained client-side in a circular buffer. Only transmitted on departure.

Edge Cases

User closes browser entirely (no beforeunload): PartyKit detects WebSocket disconnect. Send a simpler “departed” event without trail data. Other users see the dot fade out over 2 seconds with no trail.
Multiple departures in quick succession: Trails can overlap. Each is independent.
Page with no remaining users: Trail data is sent but nobody receives it. That’s fine — no persistence needed.
Very fast cursor movement before departure: Cap trail points at 30. If user was moving very fast, subsample to keep density reasonable.

Technical Architecture

Extension Side (Content Script)

interface AmbientPresenceConfig {
cursorThrottleMs: 100; // How often to send cursor position
proximityRevealStartPx: 200; // Distance at which blur starts reducing
proximityRevealFullPx: 50; // Distance at which full cursor is shown
trailBufferSeconds: 3; // Seconds of cursor history to buffer
trailFadeDurationMs: 8000; // How long trail lingers after departure
idleTimeoutMs: 60000; // Idle before dot fades
maxVisibleDots: 5; // Cap on simultaneous visible users
heartbeatIntervalMs: 30000; // WebSocket keepalive
}

interface CursorUpdate {
userId: string; // Anonymous session ID (not identity-linked)
x: number; // Viewport-relative X (percentage of viewport width)
y: number; // Viewport-relative Y (percentage of viewport height)
scrollY: number; // Page scroll position
timestamp: number;
}

interface DepartureEvent {
userId: string;
trail: CursorUpdate[]; // Last 3 seconds of positions
color: string; // User's dot color
}
Coordinate system: Send cursor positions as percentage of viewport width/height, plus scroll offset. This handles different screen sizes gracefully. Receiving side converts back to absolute pixels based on their own viewport.

PartyKit Server

use the existing partykit server

Rendering Layer

All cursor dots and trails are rendered in a single overlay div injected by the content script:
#wwo-ambient-layer {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 2147483646; /* Just below browser UI */
overflow: hidden;
}
Individual dots are absolutely positioned divs within this container. Use transform: translate3d() for GPU-accelerated positioning. CSS transition: transform 100ms linear for smooth interpolation between position updates.

Privacy & Security

No identity linking: Cursor dots use anonymous session IDs generated per page load. Not linked to playhtml identity, browsing history, or any persistent identifier.
No URL logging: The PartyKit server does not log which URLs have rooms or who connects to them.
No persistence: All data is ephemeral. Cursor positions are never stored. Trails exist only in-memory on connected clients for 8 seconds.
Extension-only: Only users with the we were online extension installed participate. No data is sent to or from the webpage itself.
User control: Extension popup includes a “quiet mode” toggle that prevents this social feature.

Metrics & Success Criteria

Quantitative:
Track: number of “proximity reveals” per user per day (how often two cursors come within 200px)
Track: average time spent on page when another user is present vs. not (does co-presence increase dwell time?)
Track: opt-out rate (what % of users disable the feature within first week)
Qualitative (beta feedback):
“Did you notice other people while browsing? How did it feel?”
“Was there any website where the dots felt annoying or out of place?”
“Did you ever try to ‘chase’ someone’s dot?”
Success = opt-out rate < 10% and at least 3 beta users organically report a positive encounter without prompting.

Scope & Non-Goals

In scope:
Blurred dot with proximity reveal
Fading departure trail
Anonymous session-based (no identity)
Works on any website
On/off toggle in extension popup
NOT in scope (future):
Cursor customization (use default dot for now)
Any form of communication between users (no chat, no reactions)
Persistent traces or heatmaps
Identity or familiar stranger detection
Sound or haptic feedback
Website-specific behavior differences

Implementation Priority

PartyKit room infrastructure — URL-based rooms, connection management, broadcast
Cursor position broadcast — throttled mousemove → WebSocket → broadcast
Blurred dot rendering — the overlay layer, dot rendering, CSS blur
Proximity reveal — distance calculation, blur interpolation
Departure trail — circular buffer, departure event, trail rendering + fade
Polish — idle timeout, viewport culling, max visible dots, toggle UI
Estimated effort: 3–4 days for a working implementation, 1–2 days for polish.

Want to print your doc?
This is not the way.
Try clicking the ··· in the right corner or using a keyboard shortcut (
CtrlP
) instead.