Habit tracking

StreakShare

Small Cheers, Big Habit Wins

StreakShare is a social habit-tracking app that transforms daily routines into live micro-commitment rooms. It serves remote knowledge workers and creators (ages 20–40) who want consistent daily habits, enabling one-tap check-ins, real-time reactions, and visible streaks that boost adherence, prevent streak decay, and free hours lost to friction.

Subscribe to get amazing product ideas like this one delivered daily to your inbox!

StreakShare

Product Details

Explore this AI-generated product idea in detail. Each aspect has been thoughtfully created to inspire your next venture.

Vision & Mission

Vision
Empower remote knowledge workers and creators to build lasting daily habits through joyful, real-time shared accountability.
Long Term Goal
Within 4 years, enable 5 million monthly active users to double daily habit retention for small groups, restoring consistent routines and reducing missed check-ins by 40%.
Impact
Increases daily habit adherence by 35% for remote knowledge workers and creators (ages 20–40), doubles routine longevity, reduces missed team check-ins by 40%, and saves individuals five hours monthly by replacing friction-filled reminders with one-tap real-time social check-ins that prevent streak decay.

Problem & Solution

Problem Statement
Remote knowledge workers and creators (ages 20–40) struggle to sustain daily habits because isolated, friction-filled check-ins and passive trackers lack immediate social reinforcement and shareable, low-effort rituals, so reminders fail to restore consistent routines.
Solution Overview
StreakShare transforms isolated routines into live micro-commitment rooms where peers perform one-tap daily check-ins and send instant reactions, making progress visible and socially reinforced to stop streak decay and restore consistent habits without extra effort.

Details & Audience

Description
StreakShare is a social habit-tracking platform that turns daily routines into shared, motivating rituals. It serves remote 20–40‑year‑old knowledge workers and creators who want consistent daily habits through social accountability. StreakShare prevents streak decay and isolation by making check-ins frictionless, visible, and rewarding, increasing adherence and freeing time. Its live habit rooms with real-time reactions expose progress and spark immediate social reinforcement.
Target Audience
Remote knowledge workers and creators (20–40) craving consistent daily habits who seek social accountability
Inspiration
I missed a two-week workout streak and felt invisible until a co-working lunch shifted everything: a colleague posted a one-line morning check-in and the whole table chimed with emojis, high-fives, and tiny micro-rewards. That instant, visible encouragement pulled me back into my routine within hours, proving that small, shared cheers — not lonely reminders — restore momentum and keep habits alive.

User Personas

Detailed profiles of the target users who would benefit most from this product.

C

Calendar-Sync Casey

- Age 28–36, remote product manager at a SaaS startup. - Heavy Google Workspace; MacBook + iPhone; Apple Watch user. - Urban co-working member with hybrid home/office routine. - Manages 20–30 meetings weekly across time zones. - Uses Calendly, Zapier, and ICS feeds to orchestrate days.

Background

Adopted time-blocking after burnout in a previous role. Tried Todoist and Notion, but friction broke habits when meetings moved. Now seeks automated, calendar-tied commitments that survive schedule churn.

Needs & Pain Points

Needs

1) Calendar-triggered check-ins with reschedule awareness. 2) Lockscreen or watch one-tap confirmation. 3) Grace windows to prevent streak penalties.

Pain Points

1) Meeting shuffles silently nuke streaks. 2) Dual calendars create duplicate, mistimed prompts. 3) Cluttered notifications bury urgent check-ins.

Psychographics

- Worships structure, hates decision fatigue. - Automation-first, minimal taps, predictable flows. - Measures success by protected focus time. - Calm aesthetics over gamified noise.

Channels

1) Google Calendar add-on 2) Slack DMs 3) Gmail inbox 4) LinkedIn feed 5) YouTube tutorials

T

Timezone-Toggling Taylor

- Age 24–34, full-stack contractor across US/EU clients. - Travels quarterly; home base Lisbon or Bali; co-lives. - Dual-SIM Android; lightweight ultrabook for mobile work. - Coordinates with multiple time zones; irregular sleep cycles. - Relies on eSIMs and airport Wi‑Fi when hopping countries.

Background

Bounced between client time zones for two years. Rigid habit apps lost streaks at midnights. Now prioritizes local-time intelligence, rolling windows, and async accountability.

Needs & Pain Points

Needs

1) Rolling check-in windows respecting local time. 2) Async reactions recap across rooms. 3) Smart midnight cutoff adjustments when traveling.

Pain Points

1) Midnight resets erase legitimate late-night effort. 2) Confusing room times across continents. 3) FOMO from missing lively sessions.

Psychographics

- Freedom first, structure kept lightweight. - Asynchronous by default, hates mandatory live. - Values progress over perfectionistic streaks. - Community connection without schedule dependence.

Channels

1) WhatsApp groups 2) Discord servers 3) X posts 4) Telegram channels 5) Nomad List forum

M

Micro-Break Morgan

- Age 26–40, customer success lead or SDR manager. - 5–7 hours per day on Zoom/Teams. - Ergonomic setup; Apple Watch or Fitbit for prompts. - Employer wellness stipend; IT-managed laptop and phone. - Works from home office with occasional hot-desking.

Background

After wrist pain and headaches, a physio prescribed micro-breaks. Timer apps nagged during calls, so they need unobtrusive, one-tap confirmations that fit between meetings.

Needs & Pain Points

Needs

1) Lockscreen/watch one-tap during DND. 2) Smart prompts between meetings, not mid-call. 3) Subtle reactions without sound or badges.

Pain Points

1) Prompts fire mid-call; can’t respond. 2) Manual logging breaks focus and flow. 3) Feels judged after missing breaks.

Psychographics

- Health-first pragmatist; values tiny, consistent wins. - Wants gentle nudges, never shaming alerts. - Prefers ambient, low-cognitive-load interfaces. - Celebrates streaks quietly, not socially.

Channels

1) Microsoft Teams app 2) Zoom marketplace 3) Slack status 4) iOS widgets 5) Apple Watch app

D

Data-Driven Devin

- Age 29–39, data analyst or growth marketer. - SQL-fluent; Notion, Airtable, Looker Studio power user. - MacBook Pro; multi-monitor dashboard workspace. - Obsidian notes; personal knowledge base maintained. - Subscribes to analytics and quantified-self newsletters.

Background

Built dashboards to correlate sleep, workouts, and output. Most habit apps hide raw data or make exports messy, hampering analysis and iteration.

Needs & Pain Points

Needs

1) CSV export and stable API endpoints. 2) Taggable check-ins and segmentable analytics. 3) Anomaly alerts protecting streak integrity.

Pain Points

1) Opaque metrics obscure adherence patterns. 2) Exports inconsistent; columns change unexpectedly. 3) No way to control confounding variables.

Psychographics

- Evidence beats vibes; decisions require data. - Loves tagging, slicing, and trend visualizations. - Enjoys experimentation with tight feedback loops. - Ownership-minded about personal datasets.

Channels

1) Hacker News threads 2) Product Hunt launches 3) X data posts 4) YouTube tutorials 5) Substack newsletters

P

Privacy-First Parker

- Age 25–40, security engineer or investigative researcher. - Firefox/Safari; iOS with network firewalls and blockers. - Proton services; hardware keys for 2FA. - Avoids mainstream socials; uses pseudonymous communities. - Self-hosts tools when possible; skeptical of telemetry.

Background

After a workplace breach exposed personal details, tightened digital hygiene. Typical habit apps feel grabby, with unclear retention and sharing defaults; needs provable privacy.

Needs & Pain Points

Needs

1) End-to-end encrypted private rooms. 2) Local-only mode with manual backups. 3) Granular consent and audit logs.

Pain Points

1) Forced public profiles expose habits. 2) Ambiguous retention and data sharing. 3) No way to verify encryption claims.

Psychographics

- Control-oriented; defaults to least privilege. - Trust earned through transparency and audits. - Values calm tools over engagement hooks. - Boundary-protective, compartmentalizes identities.

Channels

1) Proton Mail newsletter 2) Signal groups 3) Mastodon tech 4) Hacker News security 5) PrivacyGuides forum

C

Cadence Creator Cam

- Age 22–35, YouTuber, TikToker, or newsletter writer. - Monetizes via Patreon, sponsors, or affiliates. - iPhone camera-first; edits on MacBook or iPad. - Lives online; multi-platform presence daily. - Collaborates in small creator collectives and communities.

Background

Inconsistent posting stalled growth and sponsorships. Community challenges helped, but needed a personal cadence with public accountability that travels platform to platform.

Needs & Pain Points

Needs

1) Shareable streak cards formatted per platform. 2) Scheduled prompts aligned to posting windows. 3) One-tap cross-posting confirmations.

Pain Points

1) Context switching derails posting cadence. 2) Weekends disrupt rhythm and momentum. 3) Aesthetic mismatches across social platforms.

Psychographics

- Audience momentum motivates consistent output. - Prefers simple systems over complex workflows. - Seeks lightweight social proof, not vanity metrics. - Collaborates casually with fellow creators.

Channels

1) TikTok Stories 2) Instagram Stories 3) YouTube Community 4) Discord servers 5) Patreon posts

Product Features

Key capabilities that make this product valuable to its target users.

Smart Timing

Learns your check-in rhythm, calendar gaps, focus modes, and timezone shifts to trigger the rescue ping at the exact moment you’re most likely free to act. Cuts interruptions while boosting save rates—ideal for busy schedules and rolling streak windows.

Requirements

Adaptive Send-Time Model
"As a busy remote worker, I want the app to learn when I’m naturally free so that rescue pings arrive when I can actually act without breaking my flow."
Description

Build a personalized prediction model that learns each user’s check-in rhythm across habits, typical response latencies, preferred hours, and day-of-week patterns. Combine historical behavior with real-time signals (calendar availability, focus modes, timezone) to compute the highest-probability moment to trigger a rescue ping within each habit’s rolling streak window. Support cold-start heuristics, continuous online learning, and fallbacks when signals are missing. Optimize to reduce interruptions while maximizing rescue "saves," and expose model outputs to the scheduler via a ranked list of candidate send times with confidence scores.

Acceptance Criteria
Ranked Candidate Send-Times Within Rolling Window
Given a user with ≥14 days of habit check-in history and a valid rolling streak window [start, end] for today When the scheduler requests top_k=5 candidate send times for that habit Then the model returns 1–5 candidates, each timestamp ∈ [start, end] in ISO 8601 format with local timezone offset And the candidates are strictly sorted by confidence (descending); ties are ordered by earlier timestamp And each candidate includes a confidence in [0.00, 1.00] with two-decimal precision And no two candidates share the same minute And P95 generation latency from request to response is ≤300 ms
Cold-Start Heuristics Without History
Given a user-habit pair with zero historical check-ins and system cold-start config times_local = [09:00, 13:00, 18:00] And the user timezone is America/Los_Angeles and the rolling window covers the next 24 hours When the scheduler requests top_k=3 candidates Then the model returns the next occurrences of 09:00, 13:00, and 18:00 local that fall within the window (in ISO 8601) And each candidate has a confidence between 0.20 and 0.50 And if none of the configured times fall within the window, the model returns a single candidate at end_of_window − 15 minutes with confidence ≤0.30 And P95 generation latency is ≤300 ms
Real-Time Signal Re-Ranking (Calendar/Focus/Timezone)
Given an existing candidate list and any of the following signals change: a calendar event marks a candidate time as busy, a system focus mode activates, or the user timezone shifts by ≥1 hour When the signal change is received Then the model emits an updated ranked candidate list within 2 seconds (P95) and 5 seconds (P99) And candidates overlapping busy events or active focus modes are removed And all remaining candidates are within the rolling window and sorted by updated confidence And the response includes a monotonic increase in version or timestamp to indicate freshness
Rolling Window End Bias and Safety Constraints
Given a rolling streak window ending at E and more than 2 hours remaining until E When generating candidates Then at least one candidate lies within the first quartile of the remaining window and at least one within the third quartile And no candidate is after E And if less than 15 minutes remain until E, the top candidate is between now and E And if now ≥ E, the model returns an empty candidate list
Online Learning After Outcomes
Given a logged outcome event for a rescue ping labeled save (user checks in within 10 minutes) or no_save When the next candidate list is requested for the same user and habit Then the model updates online within 1 minute of outcome ingestion or flags degraded=true if the update fails And the response metadata reflects an updated model_version or updated_at timestamp > prior value And candidates near recent save times increase confidence by ≥0.02 on average and candidates near recent no_save times decrease by ≥0.02, holding other signals constant in an A/A replay test
Interruption Minimization and Quiet Hours Compliance
Given user settings max_rescue_pings_per_day=2, quiet_hours=[22:00–07:00 local], and active focus mode periods When generating candidates for any day Then the model suppresses candidates that fall within quiet_hours or active focus modes And the model returns no more than 2 candidates per calendar day with confidence ≥0.60 And if all high-confidence candidates (≥0.60) fall within suppressed periods, the model returns only candidates outside those periods even if confidence <0.60 And for each sent ping without a save within 30 minutes, the model logs interruption=true for metric computation
Scheduler Output Contract and Confidence Scores
Given the scheduler calls get_candidates(user_id, habit_id, top_k=K, now=T0) When the model responds Then the response schema contains candidates: [{timestamp:string ISO 8601 with timezone, confidence:number [0,1]}] And the list size is ≤K and deterministic for identical inputs and signals at the same T0 And confidence values for identical inputs vary by ≤0.02 across repeated calls within 60 seconds And missing_signals is present and enumerates any unavailable inputs without blocking a successful response And the response payload size is ≤5 KB for K≤5
Calendar & Availability Inference
"As a creator with a packed calendar, I want Smart Timing to use my free/busy gaps so that rescue pings land between meetings instead of during them."
Description

Integrate with major calendars (Google, Outlook, Apple) in read-only mode to infer free/busy windows and micro-gaps between events. Detect buffers (e.g., 5–10 minutes pre/post meeting), recurring focus blocks, and travel holds to avoid disruptive timing. Use privacy-preserving access (prefer free/busy vs. full event details), cache availability locally, and degrade gracefully offline. Provide a normalized availability timeline to the scheduling engine with confidence and freshness metadata.

Acceptance Criteria
Read-Only Calendar Integration & Privacy Preservation
- Given a user links Google, Outlook, or Apple Calendar, When authorization is requested, Then only read-only scopes are requested and granted (no write/delete scopes). - Given the provider supports a free/busy API, When availability is fetched, Then only free/busy data is retrieved (no titles, descriptions, attendees, or locations). - Given a provider does not offer free/busy-only scopes, When minimal metadata is needed, Then only start/end times and availability status are retrieved, never event body/attendees. - Given integration is active, When the app attempts any write or modify operation, Then the operation is blocked and no external calendar changes occur. - Given a user revokes consent, When revocation is detected, Then tokens are purged and polling stops within 60 seconds.
Micro-Gap and Buffer Detection Between Meetings
- Given two events on the same calendar day, When the gap between them is >= 3 minutes and <= 15 minutes, Then the gap is marked micro-gap=eligible and exposed to the scheduling engine. - Given any event, When within 5 minutes before start or 5 minutes after end, Then those intervals are marked as buffer=busy to suppress rescue pings. - Given overlapping buffers and meetings, When conflicts occur, Then meeting busy takes precedence over buffer labels. - Given a gap < 3 minutes, When evaluating eligibility, Then it is not marked as micro-gap. - Given a micro-gap is marked eligible, When a rescue ping is considered, Then the engine may schedule only 1 ping per gap with a max duration of 10 seconds of notification time.
Recurring Focus Blocks Recognition
- Given recurring events detected with availability status = busy and category/type indicating focus (e.g., Focus Time/Do Not Disturb) or OS-level Focus signals, When at least 3 occurrences are observed in a rolling 2-week window, Then the window is labeled focus-busy with confidence >= 0.9. - Given a focus block is active, When scheduling rescue pings, Then pings are suppressed for the full duration of the focus block. - Given the user enables the setting "allow rescue during focus (end-of-block only)", When the final 5 minutes of a focus block begins, Then at most 1 rescue ping may be sent. - Given an event titled or typed as Break within a focus series (when metadata is available), When a break segment exists, Then rescue pings are allowed within the break window only. - Given only free/busy data is available (no titles/types), When recurring daily busy blocks at the same time ±10 minutes are detected 3+ times/week, Then they are classified as focus-busy with confidence >= 0.7.
Travel Holds and Out-of-Office Detection
- Given an event with availability status = out_of_office or oof (provider-native), When building the timeline, Then the interval is labeled oof-busy and rescue pings are suppressed. - Given provider exposes travel time blocks, When such blocks are present, Then intervals are labeled travel-busy and rescue pings are suppressed. - Given only free/busy data is available, When contiguous busy spans exceed 90 minutes starting or ending outside working hours, Then label the span travel-probable with confidence = 0.6 and suppress pings. - Given any oof or travel-busy label overlaps with meetings, When resolving precedence, Then oof/travel takes precedence over meeting busy. - Given the user manually overrides a day to "travel mode" in-app, When generating the timeline, Then all working-hour intervals are labeled travel-busy (confidence = 1.0) until the override ends.
Local Caching and Offline Degradation
- Given a successful calendar sync, When availability data is stored, Then it is cached locally encrypted-at-rest and keyed per provider account. - Given the device is online, When polling calendars, Then the normalized availability timeline freshness timestamp is updated at least every 5 minutes. - Given the device goes offline, When generating the timeline, Then the engine uses the last cached timeline and decays confidence by 0.1 every 6 hours offline. - Given cached data is older than 24 hours, When evaluating pings, Then calendar-based suppression is disabled and only user-defined quiet hours apply. - Given connectivity is restored, When online status is detected, Then calendars are refreshed and the timeline is rebuilt within 30 seconds.
Normalized Availability Timeline Output
- Given availability is aggregated from multiple providers, When the timeline is emitted, Then each segment includes start, end, status {free|busy|micro-gap|buffer|focus|travel|oof|unknown}, source(s), confidence [0..1], freshness_ts (ISO-8601), and timezone. - Given overlapping events from different calendars, When conflicts occur, Then precedence is focus/travel/oof > meeting busy > buffer > micro-gap > free. - Given a rolling horizon of 48 hours, When the timeline is generated, Then 100% of that window is covered with contiguous, non-overlapping segments and no gaps. - Given a reference test calendar, When verifying labels, Then at least 95% of known busy intervals are correctly labeled across providers. - Given the scheduling engine requests the timeline, When served from cache, Then response time is <= 200 ms on a mid-tier device.
Timezone Shifts and DST Transitions
- Given the device timezone changes, When detected, Then the availability timeline is regenerated within 60 seconds and all segments are converted to the new local timezone. - Given events in foreign timezones, When normalizing, Then the engine preserves event-local start/end but exposes converted local times for scheduling decisions. - Given a DST forward or backward transition within the next 7 days, When building segments, Then there are no overlapping or missing intervals; segment boundaries align with UTC and local time correctly. - Given a timezone change is detected, When rescue pings are evaluated during the next 30 minutes, Then pings are suppressed to avoid misfires. - Given cross-midnight events, When rendering the timeline, Then segments spanning midnight are split at 00:00 local to maintain day-based queries.
Focus/DND Awareness
"As someone who uses Do Not Disturb to deep work, I want pings held until I’m out of focus so that I’m not interrupted mid-session."
Description

Detect OS-level Focus/Do Not Disturb states and in-app focus sessions to pause or defer rescue pings until an appropriate release window. Respect platform capabilities (iOS/Android/desktop), detect active phone calls/screen recording/driving modes where available, and apply smart deferrals with jitter to prevent bursts on exit. Expose focus state to the scheduler as a hard constraint with a grace period and backoff strategy.

Acceptance Criteria
OS Focus/DND Defers Rescue Pings
Given OS-level Focus or Do Not Disturb is active on a supported platform, When a rescue ping is scheduled to fire, Then the ping is not delivered and is recorded as deferred with reason "OS Focus/DND". Given OS-level Focus or Do Not Disturb ends, When there are one or more deferred rescue pings, Then the system schedules the first delivery within a 3-minute release window with 15–45 seconds random jitter. Then no more than 1 rescue ping is delivered within any 2-minute period immediately following Focus/DND end.
In-App Focus Session Pauses Pings
Given the user starts an in-app focus session with a defined duration, When the session is active, Then all rescue pings are suppressed and marked deferred with reason "In-App Focus". Given the in-app focus session ends (naturally or manually), When deferred pings exist, Then the first eligible ping is delivered after a 3-minute grace period with 15–45 seconds jitter and anti-burst limits applied. Then no rescue ping is delivered during the in-app focus session regardless of underlying OS Focus state.
Active Call/Recording/Driving Suppresses Pings
Given an active phone call, or OS-reported screen recording, or driving mode is detected, When a rescue ping would fire, Then the ping is deferred and annotated with the specific blocking reason (Call, Recording, Driving). Given multiple blocking states overlap, When evaluating delivery eligibility, Then no ping is delivered until all blocking states have cleared. Given the last blocking state clears, When deferred pings exist, Then delivery follows the standard release window (3 minutes), jitter (15–45 seconds), and anti-burst rules.
Scheduler Hard Constraint, Grace Period, and Backoff
Given any focus-like blocking state is active (OS Focus/DND, in-app focus, call/recording/driving), When the scheduler evaluates rescue ping triggers, Then the focus state is treated as a hard constraint and no pings are emitted. Given the blocking state clears, When calculating the next eligible send, Then a grace period of 3 minutes (configurable 1–10 minutes) is applied before the first attempt. Given the user does not act on the first post-release ping, When retrying, Then exponential backoff is applied (initial 5 minutes, multiplier 2x, capped at 30 minutes) with 15–45 seconds jitter; timings persist across app restarts.
Capability and Permission Aware Behavior
Given the platform does not expose Focus/DND APIs or the user has not granted required permissions, When a rescue ping would fire, Then the app does not attempt unsupported detection, proceeds as if not in Focus, and surfaces a non-blocking capability notice in settings. Given the user grants the required permission, When capability changes, Then the app reflects the new capability within 5 seconds and subsequent pings honor Focus/DND constraints. Then permission prompts are shown at most once upon enabling the feature and not more than once every 30 days if previously declined.
Anti-Burst Delivery After Focus Exit
Given Focus/DND or other blocking state ends with N deferred pings queued, When scheduling deliveries, Then deliver at most 1 ping in the first 2 minutes, ensure at least 60 seconds between subsequent pings, and drop duplicates for the same habit if a ping was delivered in the last 10 minutes. Then delivery priority is the most recent habit due per user schedule; ties are broken by original scheduled time. Then each delivered ping is logged with deferral reason(s), release batch identifier, and timestamps for test verification.
Timezone & Travel Shift Detection
"As a frequent traveler, I want my rescue pings adjusted to my current timezone so that my streaks aren’t jeopardized when I fly."
Description

Automatically detect timezone changes and daylight saving transitions to realign rescue ping windows to local time. Handle travel days by widening decision windows, preventing midnight-crossing edge cases from causing missed streaks. Sync device time, server time, and habit anchors; if offline or GPS is restricted, infer shifts from system timezone changes and calendar locations. Provide deterministic rules to avoid duplicate or skipped pings during transitions.

Acceptance Criteria
Auto-realign rescue pings after timezone change
Given a user with a daily habit anchor at 21:00 local time and a rescue ping window of 30 minutes When the device’s IANA timezone changes or UTC offset changes by ≥ 15 minutes Then within 60 seconds the system realigns the schedule so the next rescue ping occurs at 21:00 ±5 minutes in the new local timezone And no rescue pings are emitted at times corresponding to the old timezone after the change And an audit event is stored with old_zone, new_zone, old_offset, new_offset, detected_at
DST spring forward: nonexistent hour resolution
Given a locale where clocks jump from 02:00 to 03:00 and the user’s anchor is 02:30 local When the DST spring-forward transition occurs Then the rescue ping is scheduled at 03:00 ±5 minutes on that local calendar day And exactly one rescue ping is emitted for the day And the streak evaluation for that day uses the post-transition local date
DST fall back: duplicate hour de-duplication
Given a locale where clocks repeat the 01:00–02:00 hour and the user’s anchor is 01:30 local When the DST fall-back transition occurs Then exactly one rescue ping is scheduled at the first 01:30 occurrence ±5 minutes And no additional ping is sent at the second 01:30 occurrence And the streak evaluation uses the earliest qualifying check-in after local midnight
Travel day widened decision window and midnight crossing
Given the user’s timezone changes by ≥ 3 hours within a rolling 24-hour period When computing the next rescue opportunity for that local day Then widen the rescue decision window to 180 minutes for that day and place exactly one rescue ping within the widened window And if the widened window straddles local midnight, a successful check-in in either segment preserves the streak for the intended rolling window And if the user checks in before the ping, the pending rescue ping is suppressed
Offline/GPS-restricted shift inference and reschedule
Given the device is offline or location/GPS is disabled When an OS timezone change event is observed or the system timezone differs from the last known timezone Then infer the shift and realign the schedule within 60 seconds using the new local timezone And upon reconnect, reconcile with server time and keep the earliest valid scheduled ping, cancelling any duplicates And if no OS event occurred, then on reconnect with a server-observed offset change ≥ 15 minutes, realign within 60 seconds
Deterministic no-duplicate/no-skip rules during transitions
Given any timezone/DST transition or widened travel-day window creates overlapping or ambiguous rescue windows When generating pings for the affected period Then assign a deterministic window_id (habit_id + local_date + window_start) and emit at most one rescue ping per window_id And ensure at least one rescue ping exists within each 24-hour rolling window post-transition And log dedup decisions with window_id and cause=overlap|fallback|realign
Rescue Ping Orchestrator
"As a user managing multiple habits, I want the app to prioritize and time pings intelligently so that I’m nudged at the best possible moment without spam."
Description

Create a central scheduler that merges model recommendations and constraints (calendar, focus, timezone, user preferences) to select the final send time per habit. Enforce global and per-habit limits (e.g., max pings/day), rolling window deadlines, jitter to reduce predictability, and exponential backoff after snoozes or misses. Guarantee idempotency, deduplicate across devices, and trigger immediate sends when windows are about to close. Log decisions and outcomes for analytics and model feedback.

Acceptance Criteria
Final Send Time Selection Under Constraints
Given a model-recommended time R for habit H and user constraints (busy calendar blocks, active focus modes, timezone offset, user preferences) And a rolling window [W_start, W_end] defined in the user's current local time When the orchestrator selects the final send time Then the chosen time C is the earliest time >= R that lies within [W_start, W_end] and outside all blocked intervals and adheres to user preferences And if no legal slot exists within [W_start, W_end], no ping is scheduled and the outcome is recorded as "no_legal_slot" And if the user's timezone changes before selection, all times are recalculated in the new local time and the same rules apply
Global and Per-Habit Ping Limits Enforcement
Given configured limits global_max_pings_per_day = G and per_habit_max_pings_per_day = Hmax in the user's local calendar day And current counters global_sent = g and habit_sent(H) = h When scheduling a new rescue ping for H Then scheduling is allowed only if g < G and h < Hmax And if either limit is reached, the ping is not scheduled and the decision is recorded with reason "limit_reached" including the counter values And counters reset at the start of the user's new local day
Rolling Window Closeout Trigger
Given habit H has a rolling window ending at W_end and a closeout threshold T And there is no pending scheduled ping for H within T When now >= W_end - T Then the orchestrator triggers an immediate send at now if it is a legal slot under constraints And if now is not a legal slot, the orchestrator schedules at the earliest legal time ≤ W_end And if no legal time exists ≤ W_end, the orchestrator does not send and records "window_missed"
Jitter Injection to Reduce Predictability
Given a base candidate time B and a configured jitter range [-J, +J] When applying jitter Then the final candidate time C = B + random_offset where random_offset ∈ [-J, +J] And C must remain within the rolling window and outside blocked intervals; otherwise choose the nearest legal time to C within the jitter band And jitter is applied at most once per scheduling decision (not re-applied on idempotent retries)
Exponential Backoff After Snooze or Miss
Given the last rescue ping for habit H was snoozed or missed And backoff parameters initial_interval = I0, multiplier = M, max_interval = Imax When scheduling the next attempt Then the next delay D_next = min(previous_delay × M, Imax) with previous_delay = I0 for the first retry And the attempt time = now + D_next adjusted to the earliest legal slot under constraints And if the adjusted time exceeds the current window, schedule in the next window start respecting limits And backoff state resets after a successful check-in or after the configured reset boundary
Idempotency and Cross-Device Deduplication
Given an idempotency key K = hash(user_id, habit_id, window_id, attempt_seq) and a dedupe_ttl = D When multiple identical scheduling or send requests with the same K are received within D Then only one ping is sent across all devices/channels and all requests return the same result reference And subsequent duplicate requests are acknowledged without sending and recorded with dedupe_outcome = "duplicate_suppressed" And verification shows exactly one delivery event for K
Decision and Outcome Logging for Analytics
Given a scheduling decision or send outcome occurs When logging the event Then the record includes mandatory fields: event_type, user_id_hash, habit_id, window_id, decision_time_utc, user_timezone, candidate_times, chosen_time, constraints_applied, reason_codes, limit_counters, jitter_applied_ms, backoff_delay_ms, idempotency_key, dedupe_outcome, device_targets, delivery_result, user_response, and latency_ms And the log is written within L milliseconds of the decision with at-least-once guarantees And any missing mandatory field causes the event to be flagged and retried until complete or timeout T_log
Timing Feedback Loop
"As a user, I want to quickly tell the app if a ping was well-timed so that future nudges arrive when they suit me better."
Description

Add lightweight feedback mechanisms (e.g., "Good timing?" thumbs up/down, quick reason codes, and one-tap snooze) to capture explicit and implicit signals. Use response latency, snoozes, dismissals, and conversions to update send-time preferences per user and habit. Feed labeled outcomes back into the model and scheduler, support A/B tests on timing strategies, and surface aggregate lift metrics (save rate, interruption rate) to inform iteration.

Acceptance Criteria
Capture Thumbs Up/Down Timing Feedback
Given a rescue ping is delivered to a user for a specific habit When the user taps "Thumbs Up" or "Thumbs Down" on the notification or in-app banner within the active ping context Then the selection is persisted within 500 ms with fields {event_id, user_id, habit_id, ping_id, platform, ui_surface, feedback_value ∈ [up, down], created_at_utc, local_timestamp, tz_offset_minutes} And the feedback prompt is not shown again for the same ping_id And failed network writes are retried with exponential backoff up to 3 attempts and surfaced to logs if ultimately unsuccessful And the interaction is accessible (screen reader labels present; actionable via keyboard and switch control)
Collect Quick Reason Codes on Negative Feedback
Given the user selects "Thumbs Down" for a rescue ping When the reason sheet is presented Then it displays at least 6 predefined reason codes: [In meeting, Driving/Commuting, Focus mode on, Busy/Heads down, Wrong timezone/Travel, Other] And the user may optionally skip selecting a reason and still complete feedback And if "Other" is selected, an optional free-text field is shown (max 120 chars) and stored as reason_text; otherwise reason_text is null And the stored payload includes {reason_code, reason_text?, ping_id, user_id, habit_id, created_at_utc} and is successfully written within 500 ms or retried up to 3 times And analytics records the selection rate and distribution of reason_code per habit
One-Tap Snooze Behavior and Logging
Given a rescue ping is visible to the user When the user taps a snooze option from {10m, 30m, 60m} Then the next eligible rescue ping for that habit is deferred by the selected duration and no additional pings for that habit fire before snooze_end_at And a snooze event is logged with {ping_id, user_id, habit_id, duration_minutes, snooze_start_at, snooze_end_at} And if the user checks in before snooze_end_at, the snooze is canceled and a canceled_by_conversion event is logged And only one active snooze per habit is permitted; a new snooze replaces the existing one And a user cannot initiate more than 3 snoozes per habit per UTC day; after the limit, the snooze control is disabled and an explanatory tooltip is shown
Implicit Signal Logging and Attribution
Given a rescue ping is scheduled and delivered When the user interacts in any way (open app, check-in, snooze, dismiss, provide feedback) or ignores the ping Then the system logs impressions and outcomes with a shared ping_id, including: impression_at, delivered_surface, response_latency_ms (time to first action), action_type ∈ [check_in, snooze, dismiss, feedback_up, feedback_down, none], conversion ∈ [true,false] where conversion=true if check-in occurs within 60 minutes of ping And all timestamps are captured in UTC with the user's tz_offset_minutes and local_timestamp And events are persisted within 1 second end-to-end and are idempotent by (user_id, ping_id, action_type) And data validation rejects events missing ping_id or habit_id and emits error logs
Adaptive Send-Time Preference Updates per User–Habit
Given explicit and implicit signals are recorded for a user–habit When at least one qualifying signal is ingested (feedback, snooze, dismiss, conversion) Then the user–habit timing preference distribution is recalculated and persisted within 15 minutes And if there are ≥5 thumbs down OR dismiss events in the same hour bucket in the last 14 days, the scheduler reduces that bucket's send probability by ≥20% (floor 5%) And if there are ≥5 conversions with response_latency_ms ≤120000 in the same bucket in the last 14 days, the scheduler increases that bucket's send probability by ≥20% (cap +40% net per calendar week) And all updates write an audit record with before/after weights, triggering_signal, and processed_at And timezone changes detected (offset shift ≥60 minutes) cause an immediate rebase of preferred buckets to the new local day without data loss
A/B Test Support for Timing Strategies
Given an experiment is configured with id, arms, and split ratios When a user–habit becomes eligible for scheduling Then assignment to an arm is made via stable hash-based randomization on (user_id, habit_id, experiment_id) honoring configured split ratios And the assignment is persisted and attached to every ping as {experiment_id, arm_id} And experiment configuration supports enabling/disabling arms and pausing enrollment without affecting already assigned pairs And analytics can filter outcomes by {experiment_id, arm_id} and compute per-arm metrics And removing an experiment cleans up assignments after end_date while preserving historical analytics
Surface Aggregate Lift Metrics
Given events have been collected for baseline and experimental arms When viewing the Smart Timing metrics dashboard Then the dashboard displays per-arm daily metrics: save_rate (conversions/pings), interruption_rate ((dismiss or thumbs_down within 2 min)/pings), avg_response_latency_ms, snooze_rate (snoozes/pings) And relative and absolute lift vs baseline are shown with 95% CIs when n≥200 pings per arm per day And metrics are filterable by habit category, platform, timezone, and date range and refresh by 09:00 UTC daily And all metric definitions are documented in-line via tooltips and match backend calculations within ±0.1%
User Controls, Quiet Hours, and Privacy Permissions
"As a privacy-conscious user, I want clear controls and consent over Smart Timing so that I get helpful pings without giving up unnecessary data."
Description

Provide a Smart Timing settings panel for enabling/disabling the feature, linking calendars, defining quiet hours, daily ping caps, and preferred nudge windows per habit. Include transparent explainability ("Why now?") and an activity log of timing decisions. Implement consent flows for calendar and system access with least-privilege scopes, allow opt-outs and data deletion, and document data retention. Ensure compliant handling of sensitive data, and provide functional fallbacks when permissions are denied.

Acceptance Criteria
Global Smart Timing Toggle
Given the user opens Settings > Smart Timing When the global Smart Timing toggle is switched Off Then all scheduled rescue pings are canceled within 60 seconds And no new pings are scheduled until toggled On And the home screen displays "Smart Timing: Off" And per-habit Smart Timing controls are disabled but retain their saved values And no timing signals are collected or stored while Off Given the global toggle is Off When it is switched On Then previous per-habit settings are restored without changes And if required permissions are missing, a non-blocking banner prompts to link without showing a system permission dialog until tapped
Quiet Hours and Focus Mode Respect
Given quiet hours are set to 22:00–07:00 local time When the current time is within quiet hours Then Smart Timing must not send any pings, including rescue pings Given the device is in Focus/Do Not Disturb mode When a ping becomes due Then the ping is deferred and an activity log entry records "deferred: focus mode" Given quiet hours or Focus mode end When there are deferred pings Then at most one deferred ping is sent within 5 minutes if within a valid nudge window and under the daily cap Given the device timezone changes When the timezone update is detected Then quiet hours and nudge windows are reinterpreted in the new local time within 5 minutes and the schedule is recomputed
Daily Ping Cap Enforcement
Given the user sets a daily ping cap N (1–5) When Smart Timing schedules notifications Then no more than N pings are delivered in the user's local day And a minimum spacing of 30 minutes between pings is enforced Given the daily cap has been reached When additional pings are considered Then they are skipped and logged as "skipped: daily cap reached" Given local midnight occurs When the date changes in the user's timezone Then the daily cap counter resets to 0 Given no cap is set by the user When Smart Timing is active Then a default daily cap of 3 is applied
Per-Habit Nudge Windows and Rolling Streaks
Given a habit has one or two preferred nudge windows configured (e.g., 08:00–10:00 and 16:00–18:00) When Smart Timing schedules pings for that habit Then pings for that habit only occur within those windows Given a rolling streak expiration is within 30 minutes and the habit has no pending ping in a window When Smart Timing evaluates a rescue opportunity Then one rescue ping may be sent in the last 30 minutes before expiration if under the daily cap and not during quiet hours or Focus mode Given a window overlaps quiet hours When saving or scheduling Then the overlapping portion is excluded from scheduling Given the user enters an invalid window where end <= start When attempting to save Then the UI blocks saving and displays a validation error Given the user changes preferred windows When the changes are saved Then the next schedule is recomputed within 2 minutes
Explainability "Why now?" and Activity Log
Given a Smart Timing notification is received When the user taps "Why now?" Then a detail view opens within 2 seconds showing timestamp, habit, outcome (sent/deferred/skipped), and up to 3 ranked reasons with plain-language explanations (e.g., calendar gap, within nudge window, under daily cap) Given the user opens Activity Log When the screen loads Then it lists the last 30 days of Smart Timing decisions with filters for date range, habit, and outcome, paginated at 50 entries per page, and supports export to CSV and JSON Given Smart Timing makes a decision (send, defer, skip) When the decision is finalized Then an activity log entry is created with reason codes, contributing signals, and flags indicating whether quiet hours, Focus mode, or cap influenced it Given there is no data due to feature being Off or user opt-out When Activity Log is opened Then an explanatory empty state is shown
Consent, Permissions, and Least-Privilege
Given the user elects to link a calendar When requesting access Then the app requests only read-only free/busy scope and explains the purpose in plain language with a link to the data retention policy; event content scopes are not requested Given a feature needs a permission (calendar or focus status) When the user enables that feature Then permissions are requested just-in-time and never in the background Given the user denies or revokes calendar or focus permissions When Smart Timing operates Then it continues using available non-sensitive signals and displays a dismissible "Limited Smart Timing" banner; no further permission prompts appear unless the user taps "Link calendar" Given tokens or sensitive settings are stored When data is persisted or transmitted Then access tokens are stored in the OS keychain/keystore, activity logs and tokens are encrypted at rest, and all network traffic uses TLS 1.2+ Given the user revokes access at the calendar provider When the next sync occurs Then revocation is detected within 60 minutes, calendar polling stops, and a log entry states "permissions revoked"
Data Deletion, Opt-Out, and Retention Policy
Given the user navigates to Privacy > Smart Timing Data When "Delete timing data" is confirmed for a habit or all habits Then local data is deleted within 1 minute, server-side deletion is completed within 24 hours, and new data collection stops immediately Given the user toggles "Opt out of Smart Timing data collection" When the toggle is On Then Smart Timing ceases storing timing signals and uses only transient on-device signals, or fully disables scheduling if the global toggle is Off Given the retention policy is viewed When the policy screen opens Then it states: activity logs retained 30 days; aggregated performance stats retained 90 days; access tokens retained only while the account is linked; users may export their data before deletion Given a deletion request has completed When the user opens Why now? or Activity Log Then only events created after the deletion timestamp are displayed

Auto-Room Link

Deep-links you straight into the specific room and habit at risk, with the micro-commitment preselected for a true one-tap check-in. If multiple habits are vulnerable, it prioritizes the highest-stakes streak and includes a 10-second Undo to avoid accidental taps.

Requirements

Secure Deep Link Generation
"As a creator using StreakShare, I want a secure link that opens the exact room and habit with my micro-commitment ready so that I can check in with one tap without navigating."
Description

Implement a backend-driven service that creates signed, single-use deep links embedding room_id, habit_id, and micro_commitment_id, with short-lived expiry (TTL) and nonce to prevent replay. Links must be user-scoped, revocable on use, and compatible with push, email, and in-app surfaces. The service validates permissions, enforces token integrity, and returns platform-agnostic URLs that route to platform-specific handlers. This ensures users land directly in the correct room and habit context while maintaining security, privacy, and integrity of check-ins.

Acceptance Criteria
Signed Token Integrity and Tamper Detection
Given a deep link token is generated for user U with room_id R, habit_id H, and micro_commitment_id M When the token is presented unchanged within TTL Then the service verifies the signature and decodes claims for R, H, and M successfully Given any part of the token (claims or signature) is altered When it is presented Then validation fails with 401/403, no navigation occurs, and the attempt is logged without sensitive data Given a token signed with an unknown or inactive key When it is presented Then validation fails and no side effects occur
Single-Use Enforcement and Replay Protection
Given a valid unused token with nonce N When it is redeemed the first time Then nonce N is marked consumed and redemption succeeds once Given the same token is redeemed again after consumption When the second request arrives Then it is rejected with 409/410, no additional side effects occur, and the attempt is logged Given multiple parallel redemption attempts for the same token When two or more requests arrive near-simultaneously Then exactly one succeeds and the rest are rejected
User-Scoped Access and Permission Validation
Given a token is bound to user U When U opens the link (authenticated or after successful login) Then access is granted and U is routed to room R, habit H with micro_commitment M preselected Given a different authenticated user V != U opens the link When the token is presented Then access is denied with 403 and no information about R or H is revealed Given user U no longer has permission to room R or habit H When the token is presented Then access is denied and the token is invalidated without revealing resource details
TTL Expiration and Server-Side Enforcement
Given a token with configured TTL T When it is redeemed at t <= issued_at + T Then redemption succeeds Given the same token is redeemed at t > issued_at + T Then redemption is rejected with 401/410 and no side effects occur Given the service issues tokens When TTL exceeds the configured maximum Then the service rejects issuance or caps TTL to the maximum and records the effective TTL
Platform-Agnostic URL Routing to Platform Handlers
Given a platform-agnostic deep link URL When opened on iOS with the app installed Then the iOS app opens to room R, habit H with micro_commitment M preselected within 2 seconds of tap Given the same URL When opened on Android with the app installed Then the Android app opens to the same context within 2 seconds of tap Given the same URL When opened on desktop or on a device without the app installed Then the web client opens to the same context; if login is required, the user is returned to the target context after authentication Given the same URL is opened from push, email, or in-app message surfaces When the user taps the link Then routing behavior and context are identical and the token remains intact
No PII in URL and Secure Transport
Given a generated deep link URL Then it uses HTTPS scheme and contains only an opaque token; user_id, room_id, habit_id, and micro_commitment_id are not present in clear text in the path or query Given the service responds to deep link requests Then it sets Strict-Transport-Security on the deep link domain and does not emit tokens in redirects or error messages Given server access logs and analytics events for deep link requests Then raw tokens and PII are not logged
No Auto Check-In and Safe Failure Modes
Given a valid deep link is opened and validated When the destination screen loads Then no check-in is recorded automatically; micro_commitment M is preselected and requires an explicit one-tap confirm to record Given a token is invalid, expired, or the user lacks permission When the link is opened Then the user sees a non-revealing error with an action to request a new link; no room or habit names are displayed Given an error occurs during redemption When the operation fails Then no partial state is created, and the user can safely retry with a new link
Contextual Preselection & One-Tap Check-in
"As a remote knowledge worker, I want the app to preselect my at-risk habit and present a single confirmation so that I can complete a check-in quickly during a busy day."
Description

On deep-link open, the client should hydrate the room context, focus the specified habit, and preselect the designated micro-commitment, presenting a single, prominent action to confirm the check-in. The flow must provide immediate local feedback, optimistic UI, and background sync to commit the check-in, with idempotency to avoid duplicates. It should handle loading states, permission errors, and expired links gracefully, minimizing friction to make check-ins truly one tap.

Acceptance Criteria
Deep Link Hydrates Room and Preselects Micro-Commitment
Given a valid Auto-Room deep link containing roomId, habitId, and microCommitmentId When the user opens the deep link from cold or warm start Then the app navigates directly to the specified room And the specified habit is scrolled into view and visually focused And the designated micro-commitment is preselected And a single primary call-to-action to confirm the check-in is prominently visible and enabled And a skeleton or loading state is shown until room data is hydrated And if cached data is present, it is rendered immediately and reconciled when fresh data arrives
One-Tap Check-in with Optimistic UI and Background Sync
Given room and habit context is hydrated with a preselected micro-commitment When the user taps the primary check-in action Then the UI immediately reflects a successful check-in (state toggled, streak incremented, CTA disabled) And a confirmation haptic and visual toast are shown And a 10-second Undo affordance with countdown is displayed And a background sync starts to commit the check-in to the server And the user can navigate away without blocking the sync
Idempotent Check-in and Duplicate Prevention
Given a check-in attempt is initiated for a habit on a specific date And an idempotency key unique to userId+habitId+date is generated When the user triggers the check-in multiple times or the app retries the request Then only one server-side check-in record is created And subsequent identical requests return a non-error idempotent response and do not create duplicates And the local UI remains in a single consistent checked-in state
Undo Check-in Within 10 Seconds
Given a check-in has been optimistically applied When the user taps Undo within 10 seconds Then the local check-in state is fully reverted (streak, badges, counts, CTA re-enabled) And any in-flight network request is cancelled where possible or its result is reconciled And if the check-in already synced, a reversal request is sent and the server state matches local And after 10 seconds the Undo action is disabled and dismissed automatically
Highest-Stakes Habit Prioritization on Multi-Vulnerable Deep Link
Given multiple habits are vulnerable for the user at link creation time When the deep link is opened Then the habit with the highest-stakes streak (per ranking rules) is focused And its micro-commitment is preselected for one-tap check-in And the UI indicates which habit was prioritized and why (e.g., streak at risk) And users can switch to other vulnerable habits without losing one-tap affordance
Expired or Invalid Deep Link Handling
Given the deep link is expired, revoked, or malformed When the user opens the link Then a non-blocking message explains the issue and the link status And the user is routed safely to a sensible fallback (e.g., room list or home) And no check-in action is preselected or auto-submitted And retry or support options are available
Permission and Access Error Handling
Given the user lacks permission to the room or habit specified in the link When the link is opened Then no restricted data is displayed And the user sees an access-denied screen with actions to Request Access and Switch Account And the flow preserves one-tap intent by deep-linking the action that will reattempt on approval And sensitive identifiers are not exposed in the UI or logs
Highest-Stakes Streak Prioritization
"As a user with multiple habits, I want StreakShare to pick the most urgent streak automatically so that I focus on what matters and avoid losing high-value streaks."
Description

Develop a scoring model that ranks vulnerable habits and selects the highest-stakes target when multiple are at risk. Inputs include time-to-deadline (user’s timezone), current streak length, user-defined importance, historical adherence, and recency. The model must be deterministic, explainable for debugging, and configurable via remote flags. The chosen target is embedded in notifications and deep links and revalidated at open time to adapt to state changes.

Acceptance Criteria
Deterministic Selection and Tie-Breaking
Given a fixed set of vulnerable habits and a fixed system time T When the scoring model runs 100 times across different processes Then the selected target habitId and totalScore are identical for every run Given two or more habits with equal totalScore When tie-breaking is applied Then the winner is chosen in this deterministic order: higher user-defined importance > shorter time-to-deadline > longer current streak length > lower habitId (lexicographic) And this order is applied consistently across runs Given no vulnerable habits exist When the scoring model runs Then no target is selected and the output target is null/empty
Factor-Weighted Scoring Validity
Given two habits differing only in time-to-deadline (A sooner than B) When scoring runs Then score(A) > score(B) by at least the configured minimum delta Given user-defined importance increases while other factors remain constant When scoring runs Then the totalScore strictly increases Given current streak length increases while other factors remain constant When scoring runs Then the totalScore strictly increases Given historical adherence worsens (lower completion rate) while other factors remain constant When scoring runs Then the totalScore increases Given the last check-in is more recent (shorter recency) while other factors remain constant When scoring runs Then the totalScore decreases Given normalized factors and weights are applied When scoring runs Then totalScore is within [0,100] and the sum of factor contributions equals totalScore within ±0.01
Explainability and Debug Payload
Given the scoring model runs with debug flag enabled When a target is produced Then the output contains totalScore, factorContributions[{factor, rawValue, normalizedValue, weight, contribution}], tieBreakersApplied[ordered], inputsTimestamp, and modelVersion Given the scoring model runs with debug flag disabled When a target is produced Then the output contains target identifiers and totalScore only, with no per-factor details Given any factor input is missing or out-of-range When scoring runs Then the explainability payload records the defaulting behavior and value used, and the run completes without error
Remote Configuration via Flags
Given a valid remote configuration with updated weights and enabled/disabled factors When the client fetches and activates the config Then the new config is effective within 60 seconds and reflected in modelVersion and effectiveAt Given an invalid configuration (e.g., weights out of allowed range or missing required factor) When activation is attempted Then the system rejects the config, retains the last-known-good, emits an error metric, and logs the rejection reason Given two active flag variants (A/B) with different weights When users assigned to each variant run scoring Then each user receives deterministic selection under their assigned variant and variantId is present in the output Given a rollback flag is toggled When the next config fetch occurs Then the system reverts to the default config within 60 seconds and records a rollback event
Timezone and Deadline Accuracy
Given the user timezone is America/Los_Angeles on a DST transition day When computing time-to-deadline for local midnight Then the deadline reflects the correct DST offset and no 23/25-hour artifact skews the score beyond the configured tolerance Given the user travels from UTC+1 to UTC−5 and the device timezone updates When the next scoring run occurs Then time-to-deadline is computed in the new timezone and previous timezone data does not persist in the ranking Given server and device clocks differ by up to 2 minutes When computing time-to-deadline Then the model uses the server-synced time source and produces consistent deadlines across devices
Notification and Deep Link Embedding
Given a highest-stakes target is selected When a push notification and deep link are generated Then the payloads include habitId, roomId, modelVersion, totalScore, inputsTimestamp, and a revalidationRequired flag Given the user taps the notification within 10 minutes When the app opens via the deep link Then it routes to the specified room and habit with the micro-commitment preselected and target identifiers match the payload prior to revalidation Given there are no vulnerable habits at generation time When notification composition is attempted Then no Auto-Room deep link is sent and a ‘no target’ reason code is logged
Revalidation at App Open
Given the app is opened via a deep link containing a target When revalidation runs with current state Then if the target remains vulnerable, the same target is confirmed; if not, the next highest-stakes target is selected or an ‘All caught up’ state is shown, and a reason code is logged Given revalidation selects a different target than the embedded one When navigation proceeds Then the user is routed to the new target within 500 ms and previousTargetId/newTargetId are recorded Given revalidation cannot complete due to network failure When the embedded target is still within a 5-minute validity window Then the app proceeds with the embedded target; otherwise it cancels auto-selection and presents a retry option
10-Second Undo Window & Idempotent Reversal
"As a cautious user, I want a brief undo window after I check in so that I can correct accidental taps without harming my streak integrity."
Description

After a one-tap check-in, display a countdown toast allowing undo within 10 seconds. If triggered, revert the check-in, restore the prior streak state, and publish corrections to room activity feeds. All actions must be idempotent across devices and sessions, with clear visual feedback and audit logging. After the window, the check-in is finalized and no longer reversible through the quick undo path.

Acceptance Criteria
Undo Toast After One-Tap Check-In
Given a user completes a one-tap check-in for a habit in a room When the server acknowledges the check-in Then display a toast with an Undo action and a visible countdown of 10 seconds (±250 ms) And the habit card and streak indicators show the checked-in state while the toast is visible And the Undo action remains tappable until the countdown reaches zero And tapping outside the toast does not cancel the availability of Undo within the 10-second window
Idempotent Undo Across Devices and Sessions
Given a check-in event has a unique event_id and idempotency key When the same user invokes Undo multiple times from one or more devices/sessions within the 10-second window Then exactly one reversal is applied to the event_id And subsequent duplicate Undo requests return success with no additional state change (no-op) And no duplicate correction entries are created in the activity feed And all active clients converge to the reverted state within 5 seconds of the first successful Undo
Accurate Streak State Restoration on Undo
Given a user’s pre-check-in state (S0) is persisted for the habit and room When the user taps Undo within the 10-second window Then the server restores S0 exactly, including streak_count, last_check_in_at, today_status, and at_risk indicators And room/leaderboard aggregates reflect S0 within 5 seconds And the micro-commitment selection returns to its pre-check-in state And all clients display the restored state consistently using the habit’s timezone rules
Activity Feed Correction Publishing on Undo
Given the original check-in created activity event E in the room feed When Undo succeeds Then create a correction event C referencing E with type reverted_check_in And mark E as reverted without deleting it And ensure E and C are visible to room members within 5 seconds and clearly labeled And reaction counts and streak badges reflect the reverted state with no double counting
Undo Window Expiration and Finalization
Given a 10-second undo window starts at the server-acknowledged check-in timestamp When the window elapses without an Undo Then hide/disable the Undo action on all clients And subsequent Undo attempts for that event return an UNDO_WINDOW_EXPIRED error (HTTP 410 or equivalent) with no state change And the check-in is finalized and not reversible via the quick undo path And the UI shows the final checked-in state with no countdown present
Offline/Intermittent Connectivity During Undo Window
Given the device loses connectivity after the check-in toast appears When the user taps Undo within the visible 10-second window but the request cannot reach the server in time Then the client retries for up to 15 seconds total And if the server receives the Undo after the 10-second window, the request is rejected as UNDO_WINDOW_EXPIRED and the client surfaces a failure message And the client re-syncs to the finalized check-in state with no duplicate feed corrections
Audit Logging and Traceability
Given any check-in or Undo attempt is processed When the operation completes (success, no-op, or rejected) Then write an audit log entry with fields: event_id, action_type, user_id, habit_id, room_id, device_id/session_id, timestamp (UTC), idempotency_key, outcome, correlation_id And exactly one audit entry exists per final outcome for a given event_id And duplicate Undo attempts share the same correlation_id and do not create multiple success entries And audit entries are queryable within 60 seconds of the operation
Cross-Platform Link Handling & Fallback
"As a mobile user, I want deep links to work reliably across iOS, Android, and web with sane fallbacks so that I can check in regardless of device state."
Description

Implement iOS Universal Links and Android App Links for cold and warm starts, plus a web handler. Support deferred deep linking for first-time installs and a secure web fallback when the app is unavailable, preserving link parameters through sign-in. Ensure robust error handling for expired/invalid links, version compatibility, and graceful routing to the correct screen state.

Acceptance Criteria
iOS Universal Link — Cold Start to Auto-Room
Given the StreakShare iOS app is not running and the user taps a valid Universal Link containing room_id, habit_id, and action=check_in_request When the link is opened on iOS 15+ with network connectivity Then the app launches and routes directly to the targeted room with the specified habit visible and the micro-commitment preselected within 3 seconds And no intermediate screens are shown prior to the target screen And the one-tap check-in control is visible but not executed automatically And a 10-second Undo option appears only after the user taps check-in And an analytics event deep_link_opened is recorded with platform=ios and context=cold_start
Android App Link — Warm Start to Auto-Room
Given the StreakShare Android app is installed, a user is signed in, and the app is running in the background When the user taps a valid Android App Link containing room_id, habit_id, and action=check_in_request Then the existing app task is brought to foreground and routes to the targeted room with the specified habit visible and the micro-commitment preselected within 2 seconds And no duplicate Activities or tasks are created And pressing the system Back key returns the user to the prior in-app screen And an analytics event deep_link_opened is recorded with platform=android and context=warm_start
Secure Web Fallback — App Not Installed
Given the user does not have the app installed or the app cannot handle the link and the user opens a valid StreakShare link over HTTPS When the web handler receives the request Then a secure web page loads with HSTS enabled and no open redirects from query parameters And if the user is not signed in, they are prompted to sign in and upon success are routed to the room view with the targeted habit highlighted and link parameters preserved And if the user is signed in, they are routed directly to the room view with the targeted habit highlighted And the URL after sign-in does not expose sensitive parameters (e.g., token) in query string And an analytics event deep_link_opened is recorded with platform=web and context=fallback
Deferred Deep Linking — First-Time Install
Given the app is not installed and the user taps a valid StreakShare link on iOS or Android When the user installs the app from the App Store/Play Store and launches it for the first time Then the app restores the original link context and routes to the targeted room with the specified habit visible and the micro-commitment preselected after successful sign-in And the link parameters are preserved across store install and first launch without user re-entry And if the link has expired by the time of first launch, an informative error screen is shown with options to request a new link or open the app home And an analytics event deferred_deep_link_resolved is recorded with platform and install_source
Invalid or Expired Link Handling
Given a StreakShare link is malformed, expired, or fails signature/host validation When the link is opened on iOS, Android, or Web Then the user sees a non-blocking error screen stating the link is invalid or expired and offering actions to open app home or request a fresh link And no navigation occurs into any room or habit context And web endpoints return an appropriate 4xx status (400/410) without revealing resource existence And an analytics event deep_link_error is recorded with error_code and platform And the app does not crash or freeze
Version Compatibility and Graceful Routing
Given a StreakShare link requires min_supported_version higher than the installed app version When the link is opened Then the user is shown an update prompt with actions Update Now (deep link to store) and Continue And choosing Continue routes to the closest supported screen (e.g., room list or room overview) without preselecting unsupported elements And after updating to a compatible version and reopening the link, the user is routed to the exact targeted screen with micro-commitment preselected And an analytics event deep_link_version_gate is recorded with installed_version and required_version
Authorization and Parameter Validation Through Sign-In
Given a signed or parameterized StreakShare link contains room_id and habit_id When the link is processed on any platform Then only approved hosts and schemes are accepted and all parameters are validated against schema and signature And if the user is not authenticated, the parameters are securely persisted and replayed post-auth without exposure in URLs And if the user lacks access to the room or habit, an Access Denied screen is shown without confirming the resource name or existence And tampered or unknown parameters are ignored and do not affect routing And an analytics event deep_link_authz_block is recorded with reason
Notification Payload Integration
"As a user receiving reminders, I want notification taps to take me straight into the target check-in flow so that I can act immediately when prompted."
Description

Enrich push notifications and in-app reminders with the signed deep link and contextual preview (habit name, time left). Tapping should route directly into the preselected check-in flow. Support deduplication, quiet hours, user preferences, and localization. Validate that payload sizes remain within platform limits and gracefully degrade if the deep link is missing or expired.

Acceptance Criteria
Deep Link Tap Routes to Preselected Check-In
Given a push notification or in-app reminder contains a valid signed deep link with roomId, habitId, expiry, and signature, When the user taps it, Then the app opens directly to the specified habit’s check-in screen in the correct room with the micro-commitment preselected within 2s on cold start and 1s on warm start. Given the user is logged out, When they tap a valid deep link notification, Then after successful authentication they are routed to the same preselected check-in screen without additional navigation. Given the deep link is validated server-side, When the app opens, Then the link parameters are honored exactly once to prevent duplicate check-ins and an analytics event records the open and resolution state.
Contextual Preview Content and Localization
Given a notification is generated for a habit at risk, Then the title includes the localized habit name and the body includes the localized time remaining (e.g., “22 min left”) computed from the user’s configured timezone. Given the app language is set to a supported non-English locale, When a notification is delivered, Then all static strings are localized and no placeholder keys (e.g., “{{habit}}”) are visible; if a translation is missing, Then English is used as a fallback. Given a right-to-left locale is active, When a notification is displayed, Then the text order and numerals are presented correctly for RTL and the message remains legible without clipping or truncation of critical fields.
Deduplication Across Channels and Retries
Given two notifications reference the same habitId, roomId, and event timestamp within a 2-minute window, When both arrive, Then only a single visible notification remains (latest wins) and duplicates are suppressed and logged. Given both a push notification and an in-app reminder would be shown for the same event, When the app is in the foreground, Then only the in-app reminder is shown; When the app is backgrounded, Then only the push is shown. Given the provider retries delivery with the same messageId, When the duplicate arrives, Then it is dropped without alerting the user and a dedup metric is incremented.
Quiet Hours Enforcement
Given the user has configured quiet hours, When a notification would be sent during that window, Then it is not alerted (no sound, vibration, or banner) and is deferred to the next allowed time or shown as a silent notification per policy. Given the app is foregrounded during quiet hours, When an in-app reminder is triggered, Then it renders as a non-intrusive badge/toast without sound or vibration and without interrupting current activity. Given quiet hours are disabled, When the notification event occurs, Then the notification is delivered immediately per schedule.
User Notification Preferences Respect
Given the user has globally disabled notifications, When an at-risk habit event occurs, Then no push notifications are sent to that user. Given the user has disabled notifications for a specific habit, When that habit becomes at risk, Then no notification for that habit is sent while other habits continue to notify per settings. Given the user updates notification preferences (global or per habit), When a subsequent notification is evaluated, Then the new preferences are honored within 60 seconds across devices.
Payload Size Within Platform Limits With Trimming
Given a notification payload is constructed, Then its total size including data and deep link fields does not exceed the platform-specific provider limit and the message is accepted by the provider. Given the predicted payload size would exceed the limit, When building the payload, Then non-critical fields (e.g., extended body text) are truncated at word boundaries or omitted to fit while preserving the deep link, habit name, and time-left fields. Given a payload was trimmed, Then the event is logged with bytes-before/after and the delivered notification remains readable without JSON parse errors on device.
Missing or Expired Deep Link Graceful Handling
Given the notification is tapped but the deep link is missing, expired, or fails signature verification, When the app opens, Then it does not crash and routes to a relevant fallback (e.g., the room or habit list) with an inline message indicating the action expired. Given the deep link cannot be resolved to a room or habit, When the user lands on the fallback screen, Then the user can reach the intended habit’s check-in in no more than 2 taps from the fallback. Given a deep link fails validation, Then an analytics event records the failure reason (missing, expired, invalid) and no check-in is auto-committed.
Analytics & Conversion Tracking
"As a product owner, I want end-to-end tracking of deep link opens through check-ins and undos so that we can optimize conversion and identify friction points."
Description

Track end-to-end funnel events for Auto-Room Link: notification_sent, link_opened, room_loaded, one_tap_confirmed, checkin_committed, undo_shown, undo_invoked, errors. Attribute by platform, source surface, habit_id, and user segment, respecting privacy and opt-outs. Provide dashboards and cohort views to monitor conversion, undo rates, latency, and drop-offs, enabling iteration on copy, timing, and prioritization logic.

Acceptance Criteria
Funnel Events Captured End-to-End
Given a user is targeted with an Auto-Room Link, When they traverse the flow, Then the following events are emitted in this order with a shared correlation_id: notification_sent -> link_opened -> room_loaded -> one_tap_confirmed -> checkin_committed. Given any event is retried due to network loss, When connectivity is restored, Then it is delivered exactly-once within 5 minutes and deduplicated by event_id for 24 hours. Given events are received, Then each has an event_timestamp in UTC with millisecond precision and timestamps are non-decreasing within a correlation_id.
Required Attribution Fields Present and Consistent
Given any funnel event is emitted, Then platform ∈ {iOS, Android, Web}, source_surface ∈ {push_notification, email, in_app_banner, calendar, deeplink_share}, habit_id is a valid UUID, and user_segment ∈ configured segments; none are null. Given all events within the same correlation_id, Then platform and habit_id values are identical across events; mismatches emit an errors event with error_stage="attribution_validation". Given notification_sent may lack habit_id at send time, Then habit_id is backfilled on the first downstream event and the backfill completeness ≥ 95% within 24 hours.
Privacy Opt-Outs Enforced
Given user analytics_opt_out=true, When they interact with Auto-Room Link surfaces, Then no analytics events are sent from the client and any server-submitted events for that user_id are dropped. Given a user toggles analytics_opt_out to true, Then subsequent event emission stops within 1 second and no events are stored. Given any analytics payload, Then it contains no PII fields (e.g., name, email, message text); only pseudonymous ids and allowed attributes.
Undo Window Instrumentation
Given one_tap_confirmed is emitted, Then undo_shown is emitted within 100 ms with timer_duration=10s and correlation_id matching the session. Given the user taps Undo within 10 seconds, Then undo_invoked is emitted and includes reverts_event_id referencing the checkin_committed event; no additional checkin_committed is counted for that flow. Given the Undo window expires without interaction, Then no undo_invoked is emitted and the checkin remains committed.
Latency and Drop-Off Metrics Computed
Given event timestamps are collected, Then the system computes and stores latencies: sent_to_open, open_to_load, load_to_confirm, confirm_to_commit for each correlation_id. Given the reporting dashboard, Then P50 and P95 latencies are displayed by platform and source_surface and are refreshed within 1 hour of event arrival. Given funnel data, Then drop-off rates between each step are calculated daily with filters for platform, source_surface, habit_id, and user_segment.
Error Events Structured and Actionable
Given any failure occurs in the deep-link or check-in path, Then an errors event is emitted with fields: error_stage ∈ {notification, deep_link, room_load, one_tap, commit, undo, attribution_validation}, error_code ∈ documented list, severity ∈ {warning, error}, platform, source_surface, correlation_id, habit_id (if known), and message ≤ 200 chars without PII. Given errors are logged, Then the dashboard shows error rate per 1,000 flows and top 5 error_codes by platform with drill-down to sample payloads (PII redacted).
Dashboards and Cohort Views Available
Given the analytics warehouse is populated, Then a dashboard is available showing: funnel conversion from notification_sent to checkin_committed, undo rate (undo_invoked/undo_shown), and latency P50/P95; with filters for platform, source_surface, habit_id, user_segment, notification_copy_variant, send_time_bucket, and priority_reason. Given cohort analysis requirements, Then a cohort view groups users by week of first Auto-Room Link exposure and reports 7-day and 28-day conversion to checkin_committed and undo rate by user_segment and habit_id. Given data ingestion SLAs, Then dashboard metrics are updated at least hourly and lag indicators are visible when data freshness > 90 minutes.

One-Tap Snooze

If you’re in the middle of something, defer the ping by 5, 10, or 20 minutes with a single tap. StreakShare automatically re-pings before your window closes, respecting Do Not Disturb and meeting status so you save the streak without breaking focus.

Requirements

One-Tap Snooze Action UI
"As a busy remote worker, I want to snooze a check-in with one tap so that I can stay focused and still protect my streak."
Description

Provide a seamless single-tap snooze control on push notifications, in-app banners, lock screen, and watch surfaces. A tap immediately applies the default or last-used interval and shows a brief confirmation with an undo option, minimizing friction and preventing accidental dismissals. Actions deep-link back to the relevant room check-in, work offline by queuing the intent locally, and de-duplicate across multiple incoming reminders for the same habit instance.

Acceptance Criteria
Single-Tap Snooze Applies Default/Last-Used Interval
Given a habit reminder is visible on any supported surface When the user taps the Snooze action once Then the snooze interval applied is the last-used interval for this habit; if none exists, the global default interval is applied And the re-ping is scheduled for now + interval, not later than the check-in window close time And a confirmation appears within 500 ms showing “Snoozed for {interval}”
Cross-Surface One-Tap Snooze Availability
Given a reminder appears on push notification, in-app banner, lock screen, and watch When the reminder renders on each surface Then a single-tap Snooze control is visible, enabled, and accessible via screen reader with label “Snooze” and role “button” And tapping it initiates the same snooze interval logic as other surfaces And tap target size is at least 44x44 points on phone and 40x40 on watch And haptic feedback is provided on watch upon tap
Confirmation Toast with Undo Reverts Snooze
Given the user has snoozed a reminder When the confirmation is shown Then the confirmation includes an Undo action available for 5 seconds And tapping Undo within 5 seconds cancels the snooze, restores the original reminder schedule, and shows “Snooze canceled” And if Undo is not tapped within 5 seconds, the snooze remains active
Deep-Link to Relevant Room Check-In
Given the user interacts with the snoozed reminder When the user taps the reminder body or an Open action Then the app opens directly to the specific room and check-in instance associated with the reminder And if the app is closed, it cold-starts and navigates to the check-in within 3 seconds on a median device And if the check-in window has closed, the app opens the room and displays a banner indicating the window is closed
Offline Snooze Intent Queued and Synced
Given the device has no network connectivity When the user taps Snooze Then the snooze intent is stored locally with the habit instance identifier and timestamp And the confirmation displays “Snoozed (offline)” And upon connectivity restoration before the window closes, the queued intent syncs once to the server and schedules a single re-ping And if multiple offline snooze taps occur for the same habit instance, only the most recent replaces the queued intent
De-duplication Across Multiple Reminders for Same Habit Instance
Given multiple reminders for the same habit instance are delivered across different surfaces When the user snoozes on any one surface Then all other surfaces reflect the snoozed state within 2 seconds and suppress duplicate pings And only one snooze event is recorded in analytics And only one re-ping is scheduled for the habit instance
Re-ping Scheduling Respects DND and Meeting Status
Given Do Not Disturb is active or the user is in a calendar meeting marked busy When a snoozed re-ping becomes due during the active quiet period Then the re-ping is delayed until the quiet period ends and still occurs before the check-in window closes And if the quiet period extends beyond the window close, the re-ping is not sent And no audible or vibrating alert is emitted during DND or meeting on any surface
Snooze Interval Picker & Defaults
"As a creator, I want quick access to 5, 10, or 20 minute snooze options so that I can defer pings by the right amount without thinking."
Description

Offer 5, 10, and 20 minute snooze options with a one-tap default. Allow quick access to alternate intervals via a secondary affordance (e.g., long-press or inline expander) while keeping the primary path a single tap. Remember the last used interval per habit (with a global default override), validate against remaining window time, and adjust available options dynamically when the window is short. Sync preferences across devices.

Acceptance Criteria
Apply One-Tap Default Snooze
Given an active habit reminder within its response window and a visible snooze control When the user single-taps the primary snooze control Then a snooze is scheduled using the current effective default interval for that habit And no additional UI is required And the reminder is rescheduled exactly default_interval minutes from tap time And a confirmation message displays the applied interval (e.g., “Snoozed for 10m”)
Choose Alternate Snooze Interval via Secondary Affordance
Given an active habit reminder within its response window When the user long-presses the snooze control or expands the inline snooze menu Then options for 5m, 10m, and 20m are presented And tapping any option applies that interval immediately and collapses the menu And the primary one-tap path remains a single tap without additional steps
Remember Last Used Interval Per Habit with Global Default
Rule: Effective default interval precedence = per-habit last-used (if set) else global default (if set) else app default. Given Habit A has no per-habit last-used interval and a global default of X minutes is set When a reminder for Habit A appears Then the one-tap default applies X minutes Given Habit B has a last-used interval of Y minutes When the user one-taps snooze for Habit B Then the one-tap default applies Y minutes regardless of global default Given the user selects 20 minutes for Habit C via the alternate options When the next reminder for Habit C appears Then the one-tap default for Habit C is 20 minutes
Validate Against Remaining Window and Dynamic Options
Given the remaining window time is R minutes When the user opens the alternate intervals UI Then any option with duration > R is disabled or hidden And the one-tap default auto-adjusts to the longest available option <= R And if no option <= R exists (R < 5 minutes) Then the primary snooze control is disabled and a hint states “Not enough time to snooze” Given the user selects an interval D where D <= R Then the rescheduled reminder time is now + D and does not exceed the window end
Sync Snooze Preferences Across Devices
Given the user changes the global default interval on Device A while online Then the change is persisted to the cloud within 10 seconds And the same global default reflects on Device B (same account) within 30 seconds Given the user updates the last-used interval for Habit A on Device A while offline When connectivity is restored Then the update uploads within 10 seconds And appears on Device B within 30 seconds And conflicts resolve via last-write-wins using server timestamp
State Persistence and Deletion Behavior
Given a habit is deleted Then its per-habit last-used interval is removed from local storage immediately and from server storage within 60 seconds Given the user resets snooze settings to defaults Then all per-habit last-used intervals are cleared And the global default reverts to the app default value Given a habit is renamed or duplicated Then the last-used interval remains bound to the habit’s unique ID and is not copied to a new habit with a different ID
Intelligent Re-Ping Scheduler
"As a user, I want the app to automatically re-ping me at the right time so that I don’t miss my check-in after snoozing."
Description

Implement a scheduler that computes the next reminder time using the chosen snooze interval, local timezone and DST rules, device availability, and the habit window end time. Ensure re-pings fire before the window closes, cancel pending jobs upon check-in, and coalesce multiple snoozes into a single up-to-date trigger. Support server-driven push with client-side local fallback, with durable persistence to survive app restarts and brief offline periods.

Acceptance Criteria
Schedule Snoozed Re-Ping Before Habit Window Close
Given a user snoozes a reminder by X ∈ {5,10,20} minutes and the habit window ends at windowEnd When the scheduler computes the next ping Then scheduleTime = min(now + X minutes, windowEnd - 30 seconds) and the reminder fires within ±5 seconds of scheduleTime And scheduleTime is strictly earlier than windowEnd And if min(...) <= now, scheduleTime is set to now + 5 seconds (minimum delay)
Coalesce Multiple Snoozes Into Single Trigger
Given a re-ping is scheduled for T1 and the user snoozes again with X2 minutes before it fires When the scheduler updates the pending job Then exactly one pending job exists with scheduleTime = min(now + X2 minutes, windowEnd - 30 seconds) And the previous job is canceled within 1 second And only one notification is delivered within ±5 seconds of the latest scheduleTime; no earlier notification from the superseded schedule is observed
Cancel Pending Re-Pings On Successful Check-In
Given one or more re-pings are pending before windowEnd When the user completes a habit check-in Then all pending jobs are canceled within 1 second And no re-ping notification is delivered from those jobs before or after windowEnd And any server-side scheduled push with the same dedup key is suppressed
Timezone and DST Correct Scheduling
Given the device timezone observes a DST transition or the timezone changes between scheduling and firing When the user snoozes by X minutes Then the scheduler computes scheduleTime using local timezone rules so the absolute fire time equals now + X minutes, bounded by windowEnd - 30 seconds And the reminder fires within ±5 seconds of that absolute time even if the clock jumps forward or back And if the device timezone is changed after scheduling, the job still fires at the originally computed absolute UTC timestamp (bounded by windowEnd - 30 seconds) without drift
Respect Do Not Disturb and Meeting Status
Given Do Not Disturb (DND) or an active meeting status is ON at scheduleTime When the re-ping would fire Then no audible or haptic alert is produced while DND/meeting remains ON And if DND/meeting turns OFF at least 60 seconds before windowEnd, the reminder is delivered within 5 seconds of DND/meeting turning OFF And if DND/meeting remains ON until less than 60 seconds before windowEnd, a silent notification is delivered at windowEnd - 30 seconds And in all cases, no notification is delivered after windowEnd
Server Push With Local Fallback And At-Most-Once Delivery
Given a re-ping is scheduled for scheduleTime with a deduplication key When server connectivity is available Then a server push is enqueued and a client-side local job is also scheduled for scheduleTime And if the server push is received before scheduleTime, the local job is canceled within 1 second And if the server push is not received by scheduleTime, the local job delivers the reminder within ±5 seconds of scheduleTime And any late server push with the same dedup key arriving within 30 seconds after scheduleTime is ignored so that exactly one notification is shown And the reminder never fires later than windowEnd
Durable Persistence Across Restarts And Brief Offline
Given a re-ping is scheduled and the app is force-quit or the device goes offline for up to 30 minutes When the app restarts or connectivity returns Then the pending re-ping is restored from durable storage with its original scheduleTime and dedup key And if the resume occurs before scheduleTime, the reminder fires within ±5 seconds of scheduleTime (or immediately if within 5 seconds of scheduleTime) And if scheduleTime was missed but windowEnd has not passed, the reminder is delivered immediately upon resume and no later than windowEnd - 30 seconds And if windowEnd has passed, no re-ping is delivered and the job is marked expired in storage
DND and Calendar Awareness
"As a professional in meetings, I want snoozed pings to respect my DND and calendar status so that I’m not interrupted but still reminded in time."
Description

Respect OS Focus/Do Not Disturb modes and calendar presence by deferring re-pings during active silencing or meetings and delivering as soon as those states clear while still honoring the habit window. Request only necessary permissions, gracefully degrade when unavailable, and never bypass system-level silencing. Log deferral reasons and timing adjustments for observability and support.

Acceptance Criteria
Re-ping Deferred During Active OS Do Not Disturb
Given a re-ping is scheduled at T1 within the user’s habit window And the device’s OS Focus/Do Not Disturb (DND) is active at T1 When the re-ping trigger occurs Then the app must not deliver any audible, haptic, or heads-up notification at T1 And the app schedules the next attempt at the earliest time ≥ T1 when DND is inactive and ≤ window end And a deferral event is recorded with reason=DND, originalTime=T1, nextAttemptTime=T2 And if DND remains active through window end, no re-ping is delivered and finalOutcome=SuppressedByDND is recorded
Re-ping Deferred During Busy Calendar Meeting
Given calendar permission is granted And a calendar event with availability=Busy or Out of Office is active at the scheduled re-ping time T1 When the re-ping trigger occurs at T1 Then the app suppresses the re-ping at T1 And the app schedules the next attempt at the earliest time ≥ T1 when no Busy/OOO event is active and ≤ window end And a deferral event is recorded with reason=CalendarBusy, originalTime=T1, nextAttemptTime=T2 And if Busy/OOO status persists through window end, no re-ping is delivered and finalOutcome=SuppressedByCalendar is recorded
Re-ping Resumes When All Suppressing States Clear
Given both OS DND and a Busy/OOO calendar meeting overlap the scheduled re-ping time T1 When only one of the suppressing states clears Then the app continues to defer the re-ping When all suppressing states have cleared before window end Then the re-ping is delivered within 60 seconds of clearance and before window end And the deferral log is updated with reason=Both and finalOutcome=Delivered And if both states remain active until window end, finalOutcome=SuppressedByBoth is recorded and no late delivery occurs
Habit Window Honored When Suppression Extends Past Close
Given a user’s habit window ends at W_end and a re-ping is deferred due to DND and/or Busy meeting When the suppressing state(s) clear after W_end Then the app does not deliver a re-ping after W_end And the system does not auto-extend the habit window And a log entry is recorded with finalOutcome=WindowExpired and lastKnownSuppressor in {DND, CalendarBusy, Both} And the user is not notified until the next valid habit cycle
Minimal Permissions and Graceful Degradation
Given the feature initializes on a new device When requesting access Then the app requests only: calendar availability (read-busy/free) and notification policy state (read-only) as required by the OS And the app does not request microphone, contacts, full calendar details, or bypass/critical alert privileges When any required permission is denied or unavailable Then the app continues to schedule re-pings on time without attempting to infer DND or calendar state And the app never elevates notification channels or uses APIs that bypass OS silencing And a telemetry event is recorded with reason=PermissionUnavailable for any missed deferral opportunity
Deferral Observability and Audit Log
Given any re-ping is deferred or suppressed due to DND and/or CalendarBusy When the deferral decision is made Then an event is logged with fields: habitId, userId, originalScheduledTime, windowStart, windowEnd, suppressors, nextAttemptTime (nullable), decisionLatencyMs, finalOutcome And the event is available to support tooling within 2 minutes of occurrence And the system retains these events for at least 30 days And logs can be correlated to the delivered notification by notificationId when finalOutcome=Delivered
Streak Window Safeguards
"As a streak-focused user, I want the app to keep me inside my check-in window so that I can save my streak even when I’m busy."
Description

Enforce guardrails that prevent snoozes from exceeding the allowed check-in window. Dynamically cap snooze length, autoshrink intervals to fit remaining time, and surface clear “last chance” messaging when the window is nearly closed. If the window will close during DND, queue the earliest permissible re-ping and present a post-window summary explaining outcomes if the streak is lost.

Acceptance Criteria
Dynamic Snooze Cap Within Remaining Check-in Window
Given an active check-in window ending at T_end And the current time is T_now < T_end And the user selects a snooze duration D_select (e.g., 5, 10, 20 minutes) When the user taps Snooze Then the scheduled re-ping time is set to the earlier of (T_now + D_select) and the last permissible moment strictly before T_end And the effective snooze duration displayed equals scheduled re-ping time minus current time And the app does not schedule any re-ping at or after T_end
Autoshrink Snooze Options to Fit Remaining Time
Given remaining time R = T_end - T_now When the snooze menu is rendered Then only snooze options with duration <= R are displayed And if no default option fits, a single option to snooze for R rounded down to the nearest whole minute is displayed And if R < 1 minute, all snooze options are disabled and "Check in now" is the primary action
Queued Re-ping While Do Not Disturb or Meeting Is Active
Given a scheduled re-ping time T_ping occurs during an active Do Not Disturb (DND) or Busy meeting period When the system blocks interruptions Then the app queues the re-ping for the earliest permissible time before T_end And if such a time exists, the re-ping fires at that time And the re-ping occurs strictly before T_end And no notification is delivered during the blocked period
Last-Chance Messaging as Window Nears Close
Given remaining time R <= 60 seconds before T_end When a re-ping is delivered Then the notification/banner includes a clear "Last chance" label and the live remaining time And the primary action is "Check in" And any snooze action is hidden or disabled if it cannot produce a re-ping strictly before T_end
Post-Window Outcome Summary After Streak Loss
Given the check-in window has closed at T_end And no successful check-in occurred for this window When the app is able to present to the user Then a post-window summary is presented within 30 seconds of T_end or on next app foreground if notifications are blocked And the summary states whether DND/meeting was active during scheduled re-pings, lists the last attempted re-ping time, and explains that the streak was lost And no further re-pings are scheduled for the closed window
No Re-ping Possible Before Close Due to Continuous DND/Busy
Given DND or Busy meeting status is active continuously from now until after T_end When the user taps any snooze option Then no re-ping is scheduled during the blocked period And the user is informed that no re-ping can occur before the window closes And after T_end, a post-window summary is presented explaining the missed check-in and the blocking condition
Cross-Platform Delivery & Fallbacks
"As a user who switches devices, I want snoozes and re-pings to work reliably everywhere so that I can respond from whatever device is in front of me."
Description

Provide consistent One-Tap Snooze behavior across iOS, Android, web, and watchOS using native notification actions and deep links. Support haptics and badges where available, ensure de-duplication across a user’s devices, and fall back to reliable local notifications when push delivery fails or the client is offline. Verify compliance with platform notification policies to maintain deliverability.

Acceptance Criteria
iOS: Native Snooze Actions, Deep Link, Haptics
Given an iOS user is signed in with notifications permitted and a habit ping window is open When a push notification for that habit is delivered Then the notification displays three UNNotificationAction buttons labeled "Snooze 5m", "Snooze 10m", and "Snooze 20m" And tapping any snooze button invokes the deep link streakshare://snooze?habitId={habitId}&minutes={5|10|20} without foregrounding the app UI And a re-ping is scheduled at now + selected minutes with a tolerance of ±10 seconds And the current notification is dismissed within 1 second and the app badge increments by 1 And haptic feedback is played only if Focus/DND allows alerts; otherwise no haptic or sound is emitted And an analytics event "snooze_selected" is recorded with {platform:"iOS", habitId, minutes, deviceId, timestamp} And the re-ping notification is delivered before the habit window closes; if the selected delay would exceed the window, it is clamped to window_end - 30 seconds And the notification uses an authorized interruptionLevel (timeSensitive only when granted) and a registered UNNotificationCategory per Apple policy
Android: Native Snooze Actions with Channel/DND Compliance
Given an Android user (SDK 26+) is signed in with the "Habit Pings" notification channel enabled at IMPORTANCE_DEFAULT and a habit ping window is open When a push notification for that habit is delivered Then the notification shows three action buttons labeled "Snooze 5m", "Snooze 10m", and "Snooze 20m" And tapping any snooze button triggers a BroadcastReceiver without launching an Activity And schedules a re-ping via AlarmManager (exact if permitted, else inexact) or WorkManager at now + selected minutes with a tolerance of ±15 seconds And the current notification is dismissed within 1 second and no sound/vibration occurs while system DND is active And an analytics event "snooze_selected" is recorded with {platform:"Android", habitId, minutes, deviceId, timestamp} And the notification is assigned to the correct channel and does not request bypassDnd unless the user has explicitly enabled it in channel settings
Cross-Device De-duplication After Snooze
Given a user is signed in on multiple devices (e.g., iOS, Android, web, watchOS) with the same habit ping pending When the user snoozes that habit from any one device Then all other devices clear the corresponding notification within 2 seconds via a silent cancel message And no other device schedules a re-ping for the same pingId after the snooze event is acknowledged And at most one re-ping notification is delivered across all devices for that habit within the snooze interval And if a device is offline, it cancels its notification and any local schedules on the next sync within 5 seconds of reconnecting And analytics "notification_cleared_remote" is recorded on non-initiating devices with {platform, habitId, pingId, sourceDeviceId} And a deterministic dedup key {userId, habitId, pingId} is used to ensure idempotent processing server- and client-side
Offline/Push-Failure Local Notification Fallback
Given a device has a scheduled habit ping and is offline or the push delivery fails (APNs/FCM error) When the scheduled ping time occurs Then the client posts a local notification within 15 seconds that includes the three snooze actions with identical labels And tapping any snooze action records the snooze and schedules a local re-ping at now + selected minutes (±15 seconds) And upon connectivity restoration, the client reconciles with the server so that only one active notification/re-ping remains for the same pingId And telemetry logs the push error code (if any) and a flag fallback_source="local" for the event And duplicate notifications for the same {userId, habitId, pingId} do not occur on the device during fallback (0 duplicates in test runs) And automated tests demonstrate ≥99% local-notification delivery success in offline simulation across supported OS versions
Web: Web Push Delivery with In-App Fallback
Given a supported browser (Chrome/Edge/Firefox) with Web Push permission granted and an active service worker When a push for a habit ping is received Then the service worker shows a notification with action buttons "Snooze 5m", "Snooze 10m", and "Snooze 20m" And clicking an action sends a fetch to the backend to record the snooze and schedules the server-side re-ping for now + selected minutes And the client opens/focuses a tab at /snooze?habitId={habitId}&minutes={5|10|20} and closes the notification And an analytics event "snooze_selected" is recorded with {platform:"Web", habitId, minutes, browser, timestamp} And if Web Push is unsupported or permission is denied, an in-app banner with the same snooze options appears within 2 seconds of the user focusing the app, and selecting an option triggers the same backend call and scheduling
watchOS: Snooze from Notification with iPhone Sync
Given a paired Apple Watch receives a mirrored or native StreakShare notification for a habit ping When the user taps "Snooze 5m", "Snooze 10m", or "Snooze 20m" on the watch Then the action executes without opening the iPhone app UI, recording the snooze with {habitId, minutes, deviceId} And the watch provides haptic feedback upon action and dismisses the notification within 1 second And a re-ping is scheduled on the appropriate device (watch if standalone, else iPhone) at now + selected minutes (±10 seconds) And the paired iPhone (and other devices) clear their corresponding notifications within 2 seconds of the watch action And analytics "snooze_selected" is recorded with {platform:"watchOS", habitId, minutes, watchModel, timestamp} And behavior complies with Apple Watch notification action guidelines (no custom UI in notification, actions declared in category)
Metrics, Experimentation, and Rate Limits
"As a product owner, I want analytics and control over snooze behavior so that we can optimize reminders without overwhelming users."
Description

Instrument snooze taps, selected intervals, re-ping deliveries, and conversion to check-in; expose dashboards with cohort filters to assess focus-friendliness and streak retention. Enable server-controlled A/B tests for intervals and notification copy. Apply per-user and per-room rate limiting and quiet hours to prevent notification fatigue and comply with platform rules and user expectations.

Acceptance Criteria
Event Instrumentation for Snooze, Re-ping, and Conversion
- Given a user taps Snooze, when the event is sent, then the backend logs an event "snooze_tap" with fields: user_id, room_id, notification_id, snooze_interval ∈ {5,10,20}, app_platform ∈ {iOS, Android, Web}, app_version, client_ts, server_ts, dnd_state ∈ {on, off}, meeting_state ∈ {in_meeting, free}, experiment_variants, and request_id. - Given network retries, when duplicate request_id values are received within 5 minutes, then only one event is stored (idempotency) and duplicates are counted in a "deduped" metric. - Given a re-ping is scheduled or suppressed, when the job runs, then an event "reping_attempt" is logged with outcome ∈ {delivered, suppressed_dnd, suppressed_meeting, suppressed_quiet_hours, rate_limited, window_closed, error}, next_fire_at (if rescheduled), and linkage to source notification_id. - Given a check-in occurs within the active window after a snooze or re-ping, then an event "conversion_checkin" is recorded with attribution_id referencing the latest related "reping_attempt" or original ping if no re-ping, and conversion_latency_ms. - 99% of ingestion requests respond in ≤ 300 ms; events are queryable in analytics within 5 minutes of occurrence.
Cohorted Dashboards for Focus-Friendliness and Streak Retention
- Given an analyst opens the dashboard, when filters are applied, then cohorts can be filtered by date range, platform, timezone offset bucket, new vs returning user (7-day), room type ∈ {solo, team}, workday hour bucket, DND adoption (on/off), and experiment variant. - When filters change, then all charts update within 3 seconds at p95. - The dashboard must display metrics: snooze_tap_rate, avg_selected_interval_minutes, reping_delivery_rate, suppression_rate by reason, conversion_to_checkin_rate within 30 minutes and before window close, saved_streaks_count, and 7-day streak retention. - Given a CSV export request, then the filtered dataset downloads within 60 seconds and reconciles with on-screen totals within ±1% for rates and ±0.5% for counts. - Given a spot audit of 100 randomly sampled events, then metric computations reconcile with raw events within ±1% for rates and ±0.5% for counts.
Server-Controlled A/B Experiments for Intervals and Copy
- Given an experiment is created server-side, when a client fetches config, then variants for snooze intervals and notification copy are delivered without app update and cached with TTL 5 minutes. - Given a user is assigned to an experiment, then assignment is sticky per user+room via stable bucketing and logged via "experiment_exposure" before the first eligible notification. - When an experiment is paused or ended server-side, then clients stop using variant values within 5 minutes. - The experimentation service supports traffic allocation per variant (1–100%), country/timezone targeting, and platform targeting, and logs all allocations. - The results API computes primary metrics (conversion_to_checkin_rate, saved_streaks_count) per variant with two-sided 95% confidence intervals and exposes them to the dashboard.
Per-User and Per-Room Rate Limiting
- Enforce configurable limits: max_reping_per_user_per_24h = 6, max_reping_per_room_per_window = 2, min_interval_between_reping_minutes = 4. - Given a limit would be exceeded, then the re-ping is not sent, an event "reping_attempt" is logged with outcome = rate_limited, and the next eligible time is computed and stored. - Rate limits reset on a rolling window basis and are timezone-agnostic. - Rate-limit state survives process restarts and is consistent across instances. - p99 evaluation latency for rate-limit checks ≤ 10 ms.
Quiet Hours, DND, and Meeting Status Suppression
- Given user-defined quiet hours (local timezone), when a snooze or re-ping would fire inside quiet hours, then it is suppressed and an alternate attempt is scheduled for the earliest allowed time before the window closes; if not possible, outcome = window_closed is logged. - When device DND is on or user is in a calendar meeting (with granted permission), then notifications are not sent; suppressions are logged with reason and counted on the dashboard. - Suppression rules do not attempt more than 1 reschedule per original ping inside a 30-minute span. - Suppressions obey the rate limits and do not bypass them. - Unit and integration tests simulate DND/meeting signals across iOS/Android and verify zero deliveries during suppression conditions.
Snooze-to-Check-in Attribution and Windows
- Define the attribution rule: a check-in converts the latest outstanding ping chain for that room within the user's daily window; if multiple re-pings occurred, attribution goes to the most recent "reping_attempt" marked delivered. - The conversion window closes at the earlier of: user's room window end or 60 minutes after the last delivered re-ping. - Attribution logic must be deterministic and idempotent; repeated check-ins do not create duplicate conversions. - Analytics includes fields: attribution_type ∈ {original, reping}, conversion_latency_ms, and window_closed_reason if no conversion. - Backfill job can recompute attribution for the last 30 days within 2 hours and yields identical results to online logic on a 1,000-event sample.

Stealth Ping

Delivers a quiet, haptic-first nudge to lockscreen, watch, or widget—no intrusive banners—so you can rescue your streak discreetly during calls or deep work. Keeps accountability high while keeping interruptions low.

Requirements

Haptic-First Delivery Engine
"As a remote worker, I want a discreet vibration-only nudge so that I can stay on track without interrupting calls or deep work."
Description

Implements a notification delivery mode that prioritizes haptics over visual banners across iOS and Android. Uses low-visibility notification channels/categories to suppress intrusive banners while emitting distinct, subtle vibration patterns. Falls back to a minimal lockscreen entry with redacted content. Integrates with the existing notification service and feature flags, ensuring consistent behavior across app, watch, and widget endpoints. Respects system focus modes and accessibility settings while maintaining high accountability with minimal disruption.

Acceptance Criteria
Mobile Haptic-First Delivery Without Visual Banner
Given Stealth Ping is enabled and the app is backgrounded And the device is unlocked or showing the lockscreen When a streak-rescue reminder is scheduled to fire Then a haptic vibration is emitted within 2 seconds of the scheduled time And no heads-up/banner notification is shown And no sound is played And the notification uses a low-visibility category/channel (iOS interruptionLevel=passive; Android IMPORTANCE_LOW or lower) And a telemetry event "stealth_ping_delivered" is recorded with platform and channel metadata
Minimal Redacted Lockscreen Fallback
Given the device is locked at trigger time When the reminder fires Then a minimal lockscreen entry appears showing app name and generic label "Streak nudge" And notification preview content is redacted (no habit title, message text, or schedule details) And a single subtle haptic pulse is emitted And tapping the entry opens the app to the Streak Rescue screen within 1 second of app launch And no banner or full-screen intent is shown
Watch-First Haptic Delivery With No Banner
Given a paired smartwatch is connected, unlocked, and notifications are allowed for the app When a streak-rescue reminder fires Then the haptic is delivered on the watch within 2 seconds And the phone does not show a banner or play a sound And de-duplication prevents any additional haptic on the phone for the same event And a telemetry event "stealth_ping_route=watch" is recorded
Respect Focus Modes and Accessibility Settings
Given a system Focus/Do Not Disturb mode is active that does not allow the app When a reminder fires Then no haptic or visual notification is delivered And a telemetry event "stealth_ping_suppressed=focus" is recorded Given a Focus mode is active that allows the app When a reminder fires Then the haptic is delivered with interruption level set to passive and no sound or banner Given system haptics are disabled or Reduce Haptics/Touch Vibration is turned off in accessibility settings When a reminder fires Then no vibration is emitted and a minimal redacted lockscreen entry is shown
Feature-Flagged Delivery Mode Toggle
Given the remote feature flag "stealth_ping_haptic_first" is OFF When a reminder fires Then the app uses the default notification behavior per OS defaults (banner allowed) Given the remote feature flag "stealth_ping_haptic_first" is ON When a reminder fires Then the app uses haptic-first delivery as specified And remote flag changes take effect within 60 seconds without app restart And all deliveries include the flag version in telemetry
Cross-Endpoint Consistency and De-duplication
Given the user is signed in on multiple endpoints (phone, watch, widget) When a single reminder fires Then at most one haptic is emitted across all endpoints within a 30-second window And the watch endpoint takes precedence over the phone when connected And the widget does not emit a haptic or trigger an additional notification And if no endpoints are reachable, the reminder is retried once within 5 minutes And telemetry records a single delivery_id shared across endpoints
Distinct Subtle Haptic Patterns by Urgency
Given a reminder with urgency "normal" When it fires Then the "single short" haptic pattern is used (Android vibration: [0,60]; iOS haptic: one light impact) Given a reminder with urgency "rescue" When it fires Then the "double short" haptic pattern is used (Android vibration: [0,60,80,60]; iOS haptic: two light impacts spaced 80ms) And the selected pattern is recorded in telemetry
Lockscreen, Watch, and Widget Targets
"As a creator using a smartwatch, I want a nudge on my wrist and lock screen so that I can check in quickly without unlocking my phone."
Description

Delivers Stealth Ping to multiple low-friction surfaces: lockscreen (no banner), watchOS/Wear OS complications and notifications, and home/lock widgets. Includes deep links and secure tokens enabling immediate context-aware navigation to check-in. Ensures hidden content previews by default with configurable redaction. Harmonizes visual style to appear quiet and consistent across platforms while maintaining one-tap affordances.

Acceptance Criteria
Lockscreen Stealth Ping Delivery (No Banner, Haptic-First)
Given notifications permission is granted and the device is locked When a Stealth Ping is triggered Then exactly one haptic is emitted and no banner/toast is shown And a quiet lockscreen entry appears with redacted text and app icon only And tapping the entry opens directly to the target habit check-in via deep link within 2 seconds And no sound plays and no LED/flash is triggered And the notification clears automatically after successful check-in
Watch Stealth Ping Delivery (watchOS/Wear OS)
Given a paired watch with StreakShare enabled for notifications/complications When a Stealth Ping occurs during a phone call or Focus mode Then the watch delivers a haptic-only notification with redacted content and no audible chime And tapping opens the watch check-in view; if unavailable, it hands off to the phone check-in within 3 seconds And if a complication is installed, it shows a subtle indicator within 5 seconds and launches check-in on tap And no duplicate heads-up banner appears on the phone And the watch notification/indicator clears after check-in
Widget Tap-to-Check-In (Home/Lock Widgets)
Given the user has added a StreakShare home or lockscreen widget When a Stealth Ping occurs Then the widget shows a subtle indicator (e.g., badge/dot) within 5 seconds And tapping the widget opens directly to the correct habit check-in via deep link within 2 seconds And the indicator clears after successful check-in And no system banner or full-screen UI is presented
Secure Deep Link and Token Validation
Given the deep link includes a signed token scoped to user, habit, and check-in window When the link is invoked from lockscreen, watch, or widget Then the app validates signature, audience, expiry (<= 5 minutes), and enforces one-time use And on success navigates to the exact habit check-in preloaded for the current timebox And on failure shows a generic home with no sensitive data and logs a security event And tokens are never persisted in plaintext in local storage and are not exposed in content previews
Content Preview Redaction Defaults and Settings
Given default app settings enable "Hide previews" for Stealth Ping When a Stealth Ping is delivered to lockscreen, watch, or widget Then only redacted copy is shown (no habit names, streak counts, or times) And if the user selects "Show minimal", show non-sensitive generic text only when the device is unlocked And if the OS setting "Show Previews" is Never, always hide content regardless of app setting And wording and redaction are consistent across iOS, Android, watchOS, and Wear OS
Visual Harmony and Quiet Styling Across Surfaces
Given the defined design tokens for quiet styling (color, typography, spacing, haptic pattern) When rendering Stealth Ping on iOS, Android, watchOS, Wear OS, and widgets Then the UI uses the muted palette and type scale per tokens and avoids attention-grabbing animations And interactive targets meet minimum size (>= 44x44pt iOS, >= 48x48dp Android) And light/dark mode snapshots match design references within 1% pixel diff per platform And the haptic pattern matches the specified token on each platform
Multi-Surface Orchestration (Dedup, Latency, Retry)
Given the user has both phone and watch connected When a Stealth Ping is triggered Then at most one actionable notification is shown across devices at any time And if the watch notification is not acted on within 30 seconds, a quiet lockscreen entry is delivered to the phone And acting on any surface clears/suppresses all others within 2 seconds And end-to-end delivery latency meets p50 <= 2s and p95 <= 5s And transient delivery failures are retried up to 2 times with exponential backoff
Context-Aware Quiet Mode Detection
"As a user in meetings, I want Stealth Ping to avoid loud or visual alerts so that my flow isn’t broken."
Description

Detects active calls, screen sharing, and OS Focus/Do Not Disturb states to modulate delivery: haptic-only, defer, or retry later. Integrates with CallKit/TelecomManager, OS focus APIs, and in-app Deep Work mode. Applies configurable deferral windows and retries within the allowed rescue window, ensuring minimal disruption while sustaining accountability.

Acceptance Criteria
Active Call - Haptic-Only Stealth Delivery
Given an active call is detected via CallKit/TelecomManager When a Stealth Ping is triggered within the rescue window Then deliver a haptic-only notification to connected wearable and/or lockscreen within 2 seconds And do not display heads-up banners/alerts or play sounds on the phone And if no wearable is connected, deliver as a silent (no banner) lockscreen notification with haptic only And if the device is unlocked with the call UI foreground, suppress on-screen UI and send haptic only And record telemetry including quiet_mode="call", targets, and delivery outcome
OS Focus/Do Not Disturb - Deferred Delivery With Failsafe
Given OS Focus/Do Not Disturb is active When a Stealth Ping is triggered Then defer delivery until Focus ends or the user-configured deferral window elapses And while deferred, emit no sound, banner, or vibration And if Focus ends within the rescue window, deliver a haptic-only notification within 5 seconds And if the deferral window elapses and the rescue window is still valid, deliver a single haptic-only notification without banner And record telemetry including quiet_mode="focus", deferral duration, and final outcome
Screen Sharing/Recording - Watch-Only or Defer
Given active screen sharing or recording is detected (e.g., iOS Broadcast Upload / Android MediaProjection) When a Stealth Ping is triggered Then do not render any on-screen notification, overlay, or heads-up on the shared screen And if a paired wearable is available, deliver haptic-only to the wearable within 2 seconds And if no wearable is available, defer until sharing stops, then deliver haptic-only within 5 seconds And ensure no notification content is captured in the shared stream And record telemetry including quiet_mode="screen_share" and method chosen
In-App Deep Work Mode - Deferral and Non-Disruptive Retries
Given the user has enabled in-app Deep Work mode with a configured end time or break interval When a Stealth Ping is triggered during Deep Work Then queue the ping without sound, banner, or vibration And schedule retries using exponential backoff (e.g., 5m, 10m, 20m) within the remaining rescue window, capped at 3 attempts And if the user exits Deep Work before the deferral window ends, deliver a single haptic-only notification within 3 seconds And if the rescue window expires, drop pending pings without delivery and mark as expired And record telemetry including quiet_mode="deep_work", retry count, and final disposition
Configurable Deferral and Retry Policy Enforcement
Given user-level configuration for deferral window (1–30 minutes) and max retries (0–5) exists When the settings are updated Then validate new values against bounds and persist locally within 100 ms and sync to server within 5 seconds And subsequent pings must use the latest acknowledged settings And the retry schedule must not exceed the rescue window end time And total attempts (initial + retries) must not exceed max retries + 1 And enforce a minimum 60 seconds between attempts to prevent vibration spam And audit changes and effective values in logs
Overlapping Quiet States - Most Restrictive Decisioning and Integrity
Given multiple quiet states can overlap (Call, Focus/DND, Screen Share, Deep Work) When a Stealth Ping is triggered under overlapping states Then apply the most restrictive action in order: Defer > Wearable-only haptic > Device haptic-only And re-evaluate quiet states on each retry and upon quiet-state exit And do not deliver pings older than the rescue window And prevent duplicate delivery across targets (wearable, phone, widget) And include reason codes and timestamps in telemetry for each decision
Streak Rescue Window Logic
"As a habit tracker, I want a final discreet reminder before my streak breaks so that I don’t lose progress."
Description

Calculates individualized last-chance windows for each habit based on user timezone, preferred completion window, and streak decay rules. Schedules a just-in-time Stealth Ping before the streak would break, with at-most-once-per-habit/day enforcement and adaptive timing from historical responsiveness. Supports backend scheduling, queueing, idempotent delivery keys, and daylight saving transitions.

Acceptance Criteria
Per-Habit Last-Chance Window Calculation (Timezone + Preferred Window + Decay Rules)
Given a user in America/Los_Angeles with habit H whose preferred completion window is 06:00–22:00 local and whose streak decays at 00:00 local When computing the rescue schedule for 2025-10-01 Then the last-chance time T is set to min(22:00, 24:00) minus lead_time and expressed in UTC And T falls on the user's 2025-10-01 local date And T is >= now + minimum_lead_time (5 minutes) if computed after 06:00 local And the persisted schedule record includes habit_id, user_id, local_date, timezone, scheduled_at_utc, and lead_time_minutes
Just-in-Time Stealth Ping Delivery Before Streak Break
Given a scheduled last-chance time T for habit H on local date D When time reaches T Then a Stealth Ping is delivered within ±60 seconds of T And T is strictly before the decay deadline for D And exactly one ping event is emitted to the notification service for H on D And the ping payload is flagged as quiet and haptic-first (no intrusive banner)
At-Most-Once Per Habit/Day Enforcement with Idempotent Delivery Key
Given multiple schedule or delivery attempts for the same user_id, habit_id, and local_date D When each attempt uses delivery_key = sha256(user_id|habit_id|D|"streak_rescue") Then only one job is enqueued and processed And worker retries with the same delivery_key result in at most one delivered ping And the event log contains no more than one 'delivered' record per habit_id and D
Adaptive Lead Time from Historical Responsiveness
Given the last 7 Stealth Pings for habit H have a median completion lag of 14 minutes (ping → check-in) When computing today's schedule Then lead_time is set to 14 minutes clamped to [5, 30] And if there are fewer than 3 prior pings, lead_time defaults to 10 minutes And if the last 3 pings were ignored (no check-in within 60 minutes), increase lead_time by +5 minutes up to 30 minutes
Daylight Saving Time Transition Handling
Given America/New_York spring-forward on 2025-03-09 (02:00→03:00) and a habit with window end 02:30 When computing T for 2025-03-09 Then the end time resolves to 03:00 local and T = 03:00 minus lead_time Given America/New_York fall-back on 2025-11-02 (02:00 repeats) and a habit with window end 01:30 When computing T for 2025-11-02 Then the second 01:30 (post-shift) is used and T = 01:30(UTC-05:00) minus lead_time And in both cases, only one ping is scheduled and delivered for the habit/day
Scheduling, Queueing, Retry, and SLA
Given a job scheduled for time T When the scheduler persists the job Then it survives service restarts and is picked up by a worker before T And 99% of pings are delivered within ±60 seconds of T and 99.9% are either delivered or definitively marked as skipped per monthly SLO And if a job is dequeued at or after the decay deadline, it is marked 'skipped_out_of_window' and no ping is sent
Mid-Day Timezone Change Recalculation
Given a user changes timezone from UTC to Asia/Tokyo on 2025-10-01 at 15:00 UTC and the rescue ping for habit H has not fired When the change is saved Then the last-chance time T is recalculated immediately using Asia/Tokyo rules for the user's 2025-10-01 local date And if the recalculated T' <= now + 5 minutes, do not send; mark 'skipped_out_of_window' and schedule normally for the next day And at-most-once semantics are preserved with no duplicate ping for the day
One-Tap Check-In from Stealth Surfaces
"As a busy user, I want to confirm my check-in from the ping itself so that I save time and keep momentum."
Description

Enables instant check-in from the Stealth Ping via lockscreen long-press actions, smartwatch tap, or widget quick action. Uses short-lived signed tokens for secure, auth-light confirmation without opening the app. Provides a confirmation haptic and silently updates streak state with offline-safe queuing and conflict resolution. Falls back to opening the app for additional input when needed.

Acceptance Criteria
Lockscreen Long-Press One-Tap Check-In
Given a valid, unexpired check-in token is attached to a Stealth Ping on the lockscreen, When the user long-presses and selects "Check In", Then the habit’s streak for the current day is marked complete without opening the app, And a single confirmation haptic is delivered within 300 ms, And no audible sound or banner notification is shown. Given network connectivity is available, When the check-in is submitted, Then the server acknowledges within 700 ms p95, And local streak state is updated silently. Given the token is expired or invalid, When the user attempts the lockscreen check-in, Then a subtle failure haptic is delivered, And the app is opened to the habit’s check-in screen within 2 seconds for manual completion.
Smartwatch Tap Check-In with Haptic Confirmation
Given a Stealth Ping with a valid token is present on a paired smartwatch, When the user taps "Check In", Then the check-in is executed from the watch without opening the phone app, And a confirmation haptic is delivered within 300 ms, And no intrusive visual banner appears. Given the watch has any data path (Bluetooth, Wi‑Fi, or LTE), When the user taps "Check In", Then the check-in is transmitted and acknowledged within 1 second p95. Given the watch is offline, When the user taps "Check In", Then the check-in is queued on-device with an offline haptic pattern, And it retries automatically within 60 seconds of regained connectivity.
Widget Quick Action One-Tap Check-In
Given the StreakShare widget shows an actionable check-in with a valid token, When the user taps the widget’s quick action, Then the habit’s streak is marked complete without opening the app, And a confirmation haptic is delivered within 300 ms, And no banner or sound is produced. Given the check-in succeeds, When the widget refreshes, Then the widget reflects the completed state within 2 seconds of action. Given the token is invalid or expired, When the user taps the widget quick action, Then a subtle failure haptic is delivered, And the app opens to the habit’s check-in screen within 2 seconds.
Short-Lived Token Security and Replay Protection
Given a check-in token is issued for a Stealth Ping, Then its TTL is 90 seconds or less from issuance, And it is bound to user, habit, device, and day boundary, And it is signed so the server can validate authenticity without full session auth. Given the same token is received more than once, When the server processes the duplicate, Then no additional check-in is recorded, And the response indicates a duplicate (e.g., 409), And the client presents idempotent success UI if the original succeeded. Given a token is received after expiry or with invalid signature, When the server validates it, Then it is rejected (e.g., 401/403), And the client triggers the fallback to open the app for manual completion.
Offline Queueing and Sync for Stealth Check-Ins
Given the device is offline, When the user performs a one-tap check-in from any stealth surface, Then the action is persisted locally with a timestamp and habit ID, And a distinct offline confirmation haptic is delivered, And the item is queued for retry with exponential backoff for up to 24 hours. Given connectivity is restored, When queued items are retried, Then they are transmitted in FIFO order, And the streak is credited to the original local day based on the user’s configured day boundary. Given the queue cannot sync within 24 hours, When the app is next opened, Then the user is shown a non-intrusive prompt to resolve pending check-ins.
Conflict Resolution and Idempotency Across Surfaces
Given multiple one-tap check-ins for the same habit and day are initiated across different surfaces within a 5-minute window, When the server processes them, Then only one completion is recorded, And duplicates are deduplicated, And the earliest completion timestamp is retained for the habit timeline. Given two check-ins arrive with differing additional-data requirements for the habit, When the server evaluates them, Then exactly one app-open fallback is requested, And any subsequent attempts become no-ops with idempotent success UI. Given a queued offline check-in later collides with an already-completed day, When it syncs, Then it is discarded without altering the recorded completion time.
Fallback to App for Additional Input
Given the habit requires additional input (e.g., quantity, note, or photo), When the user attempts a one-tap check-in from a stealth surface, Then the app opens directly to the habit’s check-in screen within 2 seconds, And the attempt is pre-authorized with habit ID, token, and timestamp, And the streak is not marked complete until the user submits the additional input. Given the user cancels the in-app completion, When the fallback flow is aborted, Then no check-in is recorded, And the token cannot be reused from any surface, And stealth surfaces do not display a success haptic.
Privacy and Permission Handling
"As a privacy-conscious user, I want discreet notifications that reveal no private habit details so that my routines stay confidential."
Description

Introduces opt-in flows and granular settings for Stealth Ping, including per-habit toggles, preview redaction, and haptic intensity. Defaults to no sensitive content in notifications. Detects denied permissions and offers inline education and retry prompts. Stores consent and preferences securely, with policy-compliant data retention and auditability.

Acceptance Criteria
Initial Opt-In and Default Privacy Safeguards
Given a new user with no prior consent When they attempt to enable Stealth Ping Then the app presents an opt-in screen summarizing data use and links to the Privacy Policy Given OS notification permission is requested When the user has not granted it Yet Then no Stealth Ping is scheduled or delivered Given the user denies OS notification permission When returning to Stealth Ping settings Then the feature remains off and an inline education with a Retry/Settings deeplink is shown Given no explicit preview setting is selected When a Stealth Ping is delivered Then the notification contains no sensitive content (no habit name, notes, or streak count) and shows only a neutral label
Per-Habit Stealth Ping Toggles
Given a habit’s Stealth Ping toggle is Off When its scheduled ping time occurs Then no notification or haptic is delivered for that habit Given a habit’s Stealth Ping toggle is turned On When the next scheduled ping for that habit occurs Then the notification/haptic is delivered Given a user changes a habit’s toggle state When the state is saved Then the change takes effect within 10 seconds for any pending scheduled pings Given multiple habits have mixed toggle states When pings are due Then only habits with Stealth Ping On generate notifications/haptics Given the app restarts or the device reboots When the user returns Then per-habit toggle states persist accurately
Notification Preview Redaction Controls
Given default settings When a Stealth Ping is received on a locked device Then the lockscreen/notification shows app name only with no sensitive content Given the user selects Minimal Preview When a Stealth Ping is received on an unlocked device Then show a generic category label without habit name or notes Given the user selects Full Preview And the device is unlocked When a Stealth Ping is received Then show habit name but never include free-text notes Given any preview mode is selected When the device is locked Then only a redacted message is shown (no habit name or details) across phone, watch, and widgets Given the user changes the preview setting When the next ping occurs Then the new preview level is applied without requiring app restart
Haptic-First Delivery and Intensity Controls
Given Stealth Ping is enabled When a ping fires Then deliver haptic feedback only with no audible alert and no intrusive banner overlay Given the user selects Low, Medium, or High haptic intensity When they tap Test Haptic Then the device produces the corresponding OS-supported vibration pattern Given the device is in Do Not Disturb/Focus/Silent mode When a ping fires Then behavior respects OS mode (no sound, no banner) and only allowed haptics occur Given the user disables haptics for Stealth Pings When a ping fires Then no vibration is produced and no banner appears Given a paired smartwatch is available and active When a ping fires Then the haptic is routed to the watch per OS routing rules; otherwise to the phone
Permission Denial Detection and Retry Flow
Given OS notification permission is Denied When the user attempts to enable Stealth Ping Then show an inline explainer with benefits, a Try Again button, and a deeplink to system Settings Given the user taps Try Again When the OS prompt is shown And the user grants permission Then the feature toggle is auto-enabled and a success confirmation appears Given the user dismisses the explainer 3 times within 7 days When they revisit settings Then the app suppresses further prompts for 14 days Given any permission state changes in the OS Settings When the user returns to the app Then the UI reflects the new state within 2 seconds without requiring restart
Consent Storage, Retention, and Auditability
Given the user grants/withdraws consent or changes a related setting When the change is saved Then a record is stored with user ID, setting key, value, UTC timestamp, device identifier, app version, and policy version Given records are stored When inspected at rest Then they are encrypted using platform-approved encryption and transmitted over TLS Given an admin audit request When querying by user ID Then the last 100 consent/setting changes are retrievable within 5 seconds with exact before/after values Given a 24-month retention policy When a record reaches expiry Then it is purged by a nightly job and the purge event is logged with timestamp Given a user requests data export When the request is filed Then an export including consent and preference records is available within 7 days in a machine-readable format Given a user deletes their account When deletion completes Then all consent/preferences are deleted within 30 minutes, with only legally required records retained in anonymized form
Delivery Reliability, Rate Limiting, and Battery Optimization
"As a user, I want reliable yet minimal pings so that I’m nudged when it matters without being spammed or draining battery."
Description

Implements per-user and per-habit rate limits, exponential backoff, and delivery windows to prevent spam. Uses silent push plus local scheduling where supported, OS-appropriate priority channels, and background task APIs (BGTaskScheduler/WorkManager) to minimize battery impact. Adds monitoring for send, delivery, and conversion metrics with alerting on anomalies and idempotent retries.

Acceptance Criteria
Per-User and Per-Habit Rate Limiting
Given per-habit limit = 2/24h and per-user limit = 5/24h are configured When more than the allowed number of pings are triggered within the active window Then excess pings are suppressed without sending push or local notifications And a suppression record is logged with userId, habitId, reason=rate_limit, and nextEligibleAt And suppressed pings do not alter streak or conversion metrics And pings triggered after the window resets are eligible and deliver normally
Exponential Backoff and Idempotent Retries
Given a transient delivery failure (e.g., provider 5xx/timeout) When a ping delivery attempt fails Then retries use exponential backoff (1m, 2m, 4m, 8m) capped at 30m with maxAttempts=5 And all attempts use the same idempotency key to prevent duplicate user-visible notifications And on permanent errors (provider 4xx excluding 429), retries stop and the attempt is marked failed with reason And upon first successful delivery, any scheduled retries are cancelled and status=delivered is recorded
Delivery Windows and Quiet Hours Compliance
Given delivery window 08:00–20:00 local and quiet hours 12:00–13:00 or an active Focus/DND/phone call When a ping becomes due outside the allowed time or during quiet hours/active call Then the ping is deferred and scheduled for the next minute within the allowed window And the delivered notification uses low-interruption settings (iOS interruptionLevel=passive; Android channel IMPORTANCE_LOW) with no heads-up banner And if the window has fully elapsed for the day, the ping is skipped and logged with reason=outside_window
Silent Push with Local Scheduling Fallback
Given push permission is denied, the push provider is unreachable, or a silent push is not acknowledged within 10s When a ping is due within the allowed delivery window Then a local notification is scheduled for the next eligible minute within the window And the local notification is haptic-first where supported and appears on lockscreen/watch/widget without intrusive banners And exactly one notification is presented even if a remote confirmation arrives later And delivery path (remote|local), attemptCount, and end-to-end latency are recorded
Battery Optimization via Background Tasks and Low-Priority Channels
Given up to 10 pings are scheduled over a 24h period When background work is executed to prepare, retry, or deliver pings Then iOS uses BGTaskScheduler with requiredNetwork connectivity set appropriately; Android uses WorkManager with ExistingWorkPolicy=KEEP and expedited=false And no wakelock/background task runs longer than 5 seconds CPU time per invocation And notification channels are set to iOS interruptionLevel=passive and Android IMPORTANCE_LOW And average background wake frequency does not exceed 1 wake per 15 minutes per user And battery usage attributed to the feature is ≤1% of total device consumption over 24h (per OS battery stats)
Monitoring, Metrics, and Alerting on Anomalies
Given event collection is operational When pings are attempted and delivered Then metrics are emitted for send_attempted, delivered, and conversion with timestamps, userId, habitId, deviceType, and idempotencyKey And a dashboard shows delivery rate, p50/p95 latency, and conversion rate by platform within 15 minutes of event time And alerts page on-call if delivery rate <80% or p95 latency >60s for 15 consecutive minutes, or if anomaly detection flags a >3σ deviation from baseline And retries are linked to original attempts and excluded from duplicate counting via idempotencyKey

Rescue Ladder

An opt-in, capped escalation path that starts with a gentle push, then a subtle watch tap, followed by your chosen fallback (email, Slack, or SMS). It stops the instant you check in, maximizing saves without feeling naggy.

Requirements

Opt-in Enrollment & Preferences Management
"As a habit-focused user, I want to opt into and configure Rescue Ladder per habit so that I get timely nudges without feeling nagged."
Description

Provide user- and habit-level opt-in for Rescue Ladder with granular preferences. Users can enable Rescue Ladder per habit, select fallback channel (email, Slack, or SMS), define escalation delays between steps, set quiet hours, and configure daily/weekly caps. Include a preview of the sequence and a quick toggle to pause/resume. Persist preferences securely with sensible defaults and guardrails. Integrates with habit configuration screens, global notification settings, and the notification service to ensure user choices drive the escalation behavior.

Acceptance Criteria
Habit-level opt-in and pause/resume
Given a user views a habit's configuration, When the user enables Rescue Ladder and saves, Then Rescue Ladder is active only for that habit and a sequence preview is displayed. Given Rescue Ladder is active for a habit, When the user toggles Pause, Then no escalation steps are sent for that habit until Resume is turned on and all unsent scheduled steps are canceled. Given Rescue Ladder is paused for a habit, When the user toggles Resume, Then the next eligible sequence is scheduled according to current settings without sending any retroactive steps. Given global notifications are disabled, When the user attempts to enable Rescue Ladder for a habit, Then the UI warns that notifications are off and no steps are sent until global notifications are enabled. Given Rescue Ladder is off for a habit, When a check-in is missed, Then no Rescue Ladder escalation is initiated for that habit.
Fallback channel selection and validation
Given a habit with Rescue Ladder enabled, When the user selects Email/Slack/SMS as the fallback channel and saves, Then the selection persists for that habit and the fallback step is routed to that channel. Given Slack is not connected, When the user selects Slack as the fallback channel, Then the option is disabled or prompts to connect Slack and the setting cannot be saved until Slack is connected. Given SMS is selected as the fallback channel, When the user's phone number is unverified, Then the UI prompts verification and prevents saving until verification succeeds. Given a check-in occurs before the fallback step time, Then no fallback message is sent regardless of the selected channel.
Escalation delays and guardrails
Given a habit with a target check-in time T0, When the user sets Delay 1 and Delay 2 within 5–120 minutes each and saves, Then Step 2 is scheduled at T0 + Delay 1 and Step 3 at T0 + Delay 1 + Delay 2 in the user's local timezone. Given the user enters a delay outside 5–120 minutes or non-numeric, When attempting to save, Then validation errors are shown and the settings are not saved. Given an escalation step has already been sent, When the user reduces delays, Then only unsent future steps are rescheduled; previously sent steps are unaffected. Given the user checks in at any time, Then all remaining unsent steps for that habit's current cycle are canceled immediately.
Quiet hours enforcement
Given quiet hours are set from Qstart to Qend in the user's local timezone, When an escalation step falls within that window, Then the step is deferred to the next time outside quiet hours while maintaining step order and minimum configured spacing. Given quiet hours cross midnight, When scheduling steps, Then the system correctly defers steps across days without sending during quiet hours. Given a user checks in during quiet hours, Then no escalation steps are queued or sent for that cycle. Given the user changes quiet hours, When saving, Then the preview and future scheduled steps update to reflect the new window; previously sent steps remain unchanged.
Daily and weekly caps enforcement
Given a habit has daily cap D and weekly cap W within allowed ranges (D: 0–3, W: 0–10), When escalation steps are about to be sent, Then the system suppresses any step that would exceed D for the local day or W for the local week and records the suppression reason. Given the daily cap is reached, When additional escalation steps are scheduled that day, Then they are not sent and the preview indicates "Blocked by daily cap." Given the week boundary at 00:00 Monday local time, When calculating the weekly cap, Then counts reset at the start of the new week. Given caps are set to zero, Then no escalation steps are sent for that habit even if Rescue Ladder is enabled.
Sequence preview reflects settings
Given Rescue Ladder settings are configured, When the user opens the sequence preview, Then it displays the three steps with absolute local times, channel types, and any deferrals due to quiet hours or caps. Given the user changes any setting (delays, channel, quiet hours, caps), Then the preview updates within the same view and reflects the new schedule. Given a required integration is missing (e.g., Slack not connected), Then the preview marks the impacted step as unavailable and provides the remediation message. Given the current time is past T0, When viewing the preview, Then it shows the next eligible cycle or indicates that today's cycle is complete or blocked.
Secure persistence, defaults, and integration
Given a new habit with no prior settings, When Rescue Ladder is first enabled, Then sensible defaults are applied: Delay 1 = 15 minutes, Delay 2 = 30 minutes, fallback = Email, quiet hours = 22:00–07:00 local, caps = Daily 1, Weekly 5. Given the user saves Rescue Ladder settings, Then preferences are persisted securely to the server, are available on all signed-in devices within the same account, and survive app restarts. Given the user updates settings from one device, When another device opens the habit config, Then it displays the updated values within 60 seconds. Given the notification service schedules escalations, Then it uses the latest persisted preferences and honors global notification settings; on failure, the UI surfaces a non-blocking error and does not falsely indicate that settings are active.
Escalation Sequence Orchestration & Stop-on-Check-in
"As a user, I want the escalation to stop the moment I check in so that I don’t receive redundant nudges."
Description

Implement a server-driven state machine that orchestrates the escalation path: initial gentle push, then wearable haptic tap, then user-chosen fallback (email, Slack, or SMS) with configurable delays and jitter. The sequence must cancel instantly when a check-in event is received, halting any queued steps. Ensure idempotent job scheduling, deduplication, and at-most-once delivery per escalation window. Provide safeguards for race conditions when check-ins occur during sends. Integrates with check-in event stream, notification queue, and job scheduler.

Acceptance Criteria
Cancel Escalation Immediately on Check‑In
Given an active escalation sequence for a user within a current escalation window and at least one pending step is scheduled When a check-in event for that user/window is received via the event stream Then the escalation sequence is marked canceled within 1 second of event receipt And all pending and queued jobs for that window are removed or disabled within 1 second And no notifications are delivered after the cancellation timestamp And any worker dequeuing a job for that window must read the cancellation token and abort before external send And an audit log entry "escalation_canceled" is recorded with window_id, user_id, canceled_at, and cancel_reason=check_in
Orchestration Order, Timing, and Jitter
Given Rescue Ladder is enabled with configuration {step1: gentle_push at T0, step2: haptic_tap at D1, step3: fallback at D2} and jitter bounds of ±15% When the escalation window opens at T0 Then gentle_push is sent within 0–5 seconds of T0 And haptic_tap is scheduled for T0 + D1 with bounded jitter in [0.85*D1, 1.15*D1] And fallback is scheduled for T0 + D2 with bounded jitter in [0.85*D2, 1.15*D2] And steps execute strictly in order; a later step never precedes an earlier step And jitter is deterministic per user/window for a given configuration (same seed yields same offsets)
Idempotent Scheduling and Deduplication
Given the scheduler receives duplicate triggers or retries for the same user and escalation window When scheduling step N with key {window_id, step_type} Then at most one queue job exists with that key at any time And repeated schedule attempts for the same key return success without enqueuing additional jobs And each job includes an idempotency_key propagated to downstream senders And reprocessing a job with the same idempotency_key results in no additional external notification sends
At‑Most‑Once Delivery per Escalation Window
Given a user/window progresses through the escalation sequence with potential worker or provider retries When step sends are attempted across processes or instances Then each step delivers at most one notification per channel per window And duplicate provider webhooks or API retries do not create additional deliveries And queue, send logs, and metrics show zero cases where deliveries_count(user_id, window_id, step_type, channel) > 1 over controlled test runs
Race Condition Handling During Send
Given a check-in event arrives within ±500 milliseconds of a step’s scheduled send time When the worker evaluates the step for send Then if provider handoff has not occurred, the send is skipped and the step is marked canceled_due_to_check_in And if provider handoff already occurred, no further steps are executed and the sequence is marked canceled_post_handoff And in both cases, no more than one notification is delivered for that step and no downstream steps run And the final state is persisted within 2 seconds of the check-in event
Fallback Channel Selection and Execution
Given the user has selected a fallback channel (email, Slack, or SMS) and the channel is verified/connected When the sequence reaches the fallback step without a prior check-in Then exactly one message is sent via the selected fallback channel using the correct template and sender profile And no messages are sent via non-selected channels And if the selected channel is unverified/unavailable at send time, the fallback step is skipped, the sequence ends, and a failure reason is recorded; no alternate channels are attempted And channel-specific rate limits and opt-out/compliance flags are enforced
Channel Integrations & Consent Verification (Email/Slack/SMS)
"As a privacy-conscious user, I want to choose and verify my contact channels so that I control how I’m reached and can trust the messages."
Description

Integrate and template notifications across email, Slack, and SMS with deep links for one-tap check-in and clear unsubscribe options. Support channel linking and verification (email verification, Slack OAuth with correct scopes, SMS number verification) and log explicit consent per channel. Handle delivery status, bounces, and failures with smart failover to the next step. Apply per-channel rate limits, personalization, and localization. Integrates with providers (e.g., SendGrid/Postmark, Slack API, Twilio), user identity, preferences, and analytics.

Acceptance Criteria
Email Channel Linking, Verification, and Consent Logging
Given a user adds a new email address When they submit the address Then send a verification email with a unique single-use token valid for 24 hours and log a verification_sent event Given the user clicks the verification link within 24 hours When the token is valid and unused Then mark the email as verified and log verification_success with timestamp and IP Given an email address is not verified When a non-verification notification is attempted Then block the send and log blocked_unverified_email Given an explicit “Receive Rescue Ladder emails” consent checkbox (unchecked by default) When the user opts in and saves Then store consent=true with channel=email, timestamp, terms_version, and source=ui Given the user revokes email consent or clicks unsubscribe in any email When the request is processed Then set consent=false immediately, prevent further sends (except compliance messages), and show a confirmation page
Slack OAuth Connect with Correct Scopes and Consent Capture
Given a user chooses Connect Slack When OAuth is initiated Then request only the app-configured scopes and display the selected workspace name to the user Given Slack returns an access token and bot identity When linking completes Then store workspace_id, slack_user_id (or mapping), and DM channel reference and mark the connection active Given the connection is active and the user explicitly enables Slack notifications When they save preferences Then store consent=true with channel=slack and timestamp Given the Slack app is removed or the token is revoked When an app_uninstalled event or auth error is received Then mark consent=false, set connection inactive, and stop all Slack sends Given a Slack send is attempted without consent When evaluating send Then block the send and log blocked_no_consent_slack
SMS Number Verification and TCPA/GDPR-Compliant Consent
Given a user enters a phone number in E.164 format When they request verification Then send a 6-digit code via SMS with a 10-minute TTL and allow a maximum of 5 attempts Given the user submits the correct code within 10 minutes When validation succeeds Then mark the number verified and log sms_verification_success Given an explicit SMS consent disclosure including STOP/HELP language (unchecked by default) When the user opts in Then store consent=true with channel=sms, timestamp, IP, and terms_version Given an inbound SMS with content STOP from the verified number When received Then immediately set consent=false, send an opt-out confirmation message, and cease all further SMS sends Given the number is unverified or consent=false When a ladder SMS step is triggered Then block the send and log blocked_sms_no_verification_or_consent
Template Rendering with Localization, Personalization, Deep Links, and Unsubscribe
Given a notification is generated When rendering Then fill tokens {first_name}, {habit_name}, {streak_count}, {room_name}, and {time_left} with safe fallbacks if missing Given a user locale and timezone are available When selecting content Then choose a localized template and format dates/times in the user’s timezone; if missing, fall back to en-US Given deep links are required for one-tap check-in When generating links Then create single-use signed tokens bound to user_id and ladder_step with a default 15-minute TTL and platform-specific targets (iOS/Android/Web); if app not installed, open a responsive web fallback Given email, Slack, and SMS templates When assembling the final message Then include an unsubscribe/manage link or instruction appropriate to the channel (email one-click unsubscribe, Slack manage action, SMS “Reply STOP” text) Given preview mode is used by a tester When requesting a preview Then render the exact final content per channel without sending
Delivery Status Webhooks and Smart Failover within Rescue Ladder
Given a message send is initiated When the provider acknowledges Then create a message record with status=sent and a correlation_id Given provider webhooks are received When they indicate delivered, bounced, failed, throttled, opened, clicked, or read Then update the message status accordingly and emit analytics events idempotently Given a step bounces, fails, or has no delivery confirmation within 2 minutes When evaluating escalation Then trigger the next configured channel in the ladder and log failover_triggered Given a message is delivered but no user check-in occurs within the step window (default 5 minutes) When evaluating escalation Then proceed to the next step; if any check-in occurs, cancel all pending steps immediately Given duplicate webhooks or retries are received When processing Then ensure idempotency so statuses and escalations are applied only once per message
Per-Channel Rate Limits and Quiet Hours Enforcement
Given per-user per-channel limits are configured (default 3/day) When attempting to send Then do not exceed the limit within a rolling 24-hour window in the user’s timezone Given a user’s quiet hours are configured When current time is inside quiet hours Then defer or skip the step according to ladder policy and never send SMS or Slack during quiet hours Given channel error rate exceeds 5% over a 5-minute window When the circuit breaker evaluates Then pause further sends on that channel for 10 minutes and alert operations Given global throttles are configured When overall send rate exceeds the threshold Then queue messages and release in FIFO order without starving any single user
Unsubscribe and Preference Sync Across Channels
Given an email unsubscribe link is clicked When processed Then set email consent=false for that user and display a confirmation page within 2 seconds Given a Slack message “Manage notifications” action is used to mute the ladder When confirmed Then set slack consent=false and send an ephemeral confirmation to the user Given an inbound SMS STOP is received When processed Then set sms consent=false, send an opt-out confirmation, and cancel any queued SMS steps immediately Given any consent change occurs via channel action or preferences UI When syncing Then propagate the new state to the sending service within 10 seconds and prevent further sends accordingly Given audit logging is enabled When consent changes or sends occur Then record actor, channel, timestamp, and reason and make logs exportable for compliance
Wearable Haptic Nudge Support
"As a wearable user, I want a discreet tap on my watch with a one-tap check-in so that I can stay on track without pulling out my phone."
Description

Provide Apple Watch and Wear OS support for discreet haptic nudges with actionable notifications that allow one-tap check-in from the wearable. Manage OS permissions and notification categories, ensure low-latency delivery, and design a distinct vibration pattern. Detect wearable availability and fall back to phone push when unavailable. Sync check-in state back to mobile and server immediately to cancel remaining steps. Integrates with mobile apps, watch extensions, and the central notification service.

Acceptance Criteria
One-Tap Wearable Check-In from Haptic Nudge
- Given Rescue Ladder is enabled and a registered Apple Watch or Wear OS device is available, When a scheduled nudge is dispatched, Then an actionable notification appears on the wearable with actions "Check In", "Snooze 5m", and "Dismiss". - Given the actionable notification is displayed on the wearable, When the user taps "Check In", Then the habit is marked as checked in without requiring the phone to be unlocked or the app to be opened. - Given the user taps "Snooze 5m", When 5 minutes elapse and no check-in has occurred elsewhere, Then a single follow-up wearable notification is delivered. - Given the user taps "Dismiss", When the action is processed, Then no further Rescue Ladder steps occur for that nudge instance.
Distinct, Discreet Haptic Pattern on Wearables
- Given a wearable nudge is delivered on watchOS, When haptic feedback plays, Then it uses a two-tap sequence (notification then success) with ~200 ms gap and total duration ≤ 1.2 s, respecting system haptic intensity. - Given a wearable nudge is delivered on Wear OS 3+, When vibration plays, Then it uses VibrationEffect waveform [0,80,160,80] with amplitudes [0,140,0,140] and total duration ≤ 1.2 s, respecting system vibration settings. - Given other StreakShare notification categories exist, When their haptics play, Then their patterns differ from the Rescue Ladder pattern. - Given Theater/Silent mode is enabled on the wearable, When the nudge arrives, Then no audible alert is produced and only haptic occurs per OS behavior.
Notification Permissions and Category Setup
- Given the user enables Rescue Ladder wearable nudges, When notification permission is not granted on the phone, Then the app triggers the system permission prompt and records the result. - Given permission is denied, When the user returns to Rescue Ladder settings, Then a persistent guidance card with a deep link to OS Settings is shown and the permission state displays as Denied. - Given permissions are granted, When the app initializes, Then notification category "RESCUE_NUDGE" with actions "CHECK_IN", "SNOOZE_5M", and "DISMISS" is registered on iOS and Android and mirrored to the watch extension. - Given locale en-US, When categories are registered, Then action titles are "Check In", "Snooze 5m", and "Dismiss".
Low-Latency Delivery to Wearable
- Given the wearable is connected and reachable, When the central notification service dispatches a Rescue Ladder nudge, Then the notification appears on the wearable within 2 s P50 and 5 s P95 end-to-end in test conditions. - Given transient network loss, When a nudge dispatch fails, Then up to 2 retries occur within 10 s total before applying fallback rules. - Given the phone is locked, When the wearable is reachable, Then the notification is delivered to the wearable without requiring phone interaction.
Wearable Availability Detection and Phone Fallback
- Given no registered wearable exists or the last wearable heartbeat is older than 15 minutes, When a Rescue Ladder nudge is dispatched, Then the nudge is sent to the phone as a push notification instead of the wearable. - Given the wearable is unreachable at dispatch time, When fallback is triggered, Then the phone receives the push within 250 ms of the fallback decision and no wearable notification is attempted. - Given the wearable becomes reachable again before the next nudge, When a new nudge is dispatched, Then delivery resumes to the wearable.
Immediate Sync and Escalation Cancellation on Check-In
- Given the user checks in from the wearable notification, When the action reaches the server, Then the user's check-in status updates within 1 s and reflects in the mobile app and watch extension on next sync or push. - Given a check-in is recorded, When remaining Rescue Ladder steps (email, Slack, SMS, phone push) are pending for that nudge, Then they are canceled and not sent. - Given concurrent actions occur (wearable and phone), When both reach the server, Then only one check-in is recorded and the duplicate receives an idempotent success response.
Secure Action Handling and Cross-Platform Integration
- Given a wearable action is sent, When the server validates the request, Then it must contain a signed token scoped to the user and habit and issued ≤ 10 minutes ago; otherwise it is rejected with a recoverable error. - Given the action is accepted, When the response returns, Then the wearable notification updates to "Checked in" within 1 s and auto-dismisses within 3 s. - Given a version mismatch where the watch extension is older than the mobile app, When a Rescue Ladder nudge is delivered, Then "Check In" and "Dismiss" actions still function and the mobile app surfaces an update prompt outside the wearable UI.
Anti-Nagging Controls: Caps, Quiet Hours, Snooze
"As a user, I want limits and quiet hours so that the app respects my boundaries and work-life balance."
Description

Enforce user-respectful controls that limit frequency and timing of nudges. Support per-habit and global caps (per day and per week), quiet hours by local time, and suppression after recent engagement or decline. Provide a user-visible snooze for a habit or for all Rescue Ladder activity. Respect device Do Not Disturb and calendar busy signals where permissions exist. Expose effective caps and next eligible send time in settings. Integrates with scheduling engine, preferences, and analytics to prevent over-notification.

Acceptance Criteria
Per-Habit Caps (Daily and Weekly)
Given a habit has daily cap D and weekly cap W configured in preferences When the scheduling engine evaluates eligible Rescue Ladder sends in the user’s local time Then no more than D sends are delivered between 00:00 and 23:59 local time for that habit, and no more than W are delivered between Monday 00:00 and Sunday 23:59 local time Given additional sends would exceed a per-habit cap When the engine attempts to schedule them Then they are suppressed with reason per_habit_cap_reached and logged to analytics with habit_id, cap_type, and timestamp Given a cap boundary is crossed (local midnight or weekly boundary) When counters reset Then remaining sends are recalculated and Next eligible send is recomputed respecting quiet hours, DND, and calendar busy signals Given a send is suppressed due to a per-habit cap When the user opens the habit’s Rescue Ladder settings Then Remaining sends today/this week show 0 and Next eligible send shows the earliest compliant time in local time
Global Caps Across Habits
Given global daily cap Gd and global weekly cap Gw are enabled When multiple habits are eligible concurrently Then the engine delivers at most Gd sends per day and at most Gw per week across all habits Given multiple eligible sends compete under the global cap When prioritizing which to deliver Then the engine orders by next_eligible_time ascending, then habit_priority descending, then habit_id ascending to ensure deterministic selection Given the global cap is reached for the current day or week When new sends become eligible Then they are deferred and Next eligible send is set to the earliest boundary after which both global and per-habit caps allow a send Given a send is blocked by the global cap When analytics is recorded Then notification_suppressed is logged with reason global_cap_reached and includes affected habit_id(s)
Quiet Hours by Local Time and Time Zone Changes
Given Quiet Hours are configured with start Qs and end Qe in local time When the current local time falls within the quiet window (including overnight ranges where Qe < Qs) Then Rescue Ladder notifications are not delivered and eligible sends are deferred Given the device time zone changes When computing quiet hour membership and cap boundaries Then evaluations immediately use the new local time without requiring app restart and Next eligible send is recalculated Given a daylight saving time transition adds or removes an hour When quiet hours overlap the transition Then wall‑clock quiet times are honored and no sends occur within the labeled quiet interval Given a send is deferred due to quiet hours When viewing settings Then Next eligible send displays the first time outside quiet hours that also respects caps, DND, and calendar busy
Suppression After Recent Engagement or Decline
Given a cooldown window C minutes is configured for a habit When the user checks in for the habit or explicitly declines a Rescue Ladder prompt Then all further Rescue Ladder sends for that habit are suppressed for C minutes Given a check‑in occurs during an active escalation sequence When the check‑in is recorded Then any pending or in‑flight steps for that habit are canceled within 1 second and no further steps in the sequence are sent Given a send is suppressed by cooldown When analytics is recorded Then notification_suppressed is logged with reason recent_engagement_or_decline and includes cooldown_remaining_ms
Snooze Controls (Per-Habit and Global)
Given the user taps Snooze on a habit and selects duration S When snooze is confirmed Then no Rescue Ladder notifications for that habit are delivered until current_time + S and the UI shows an active snooze badge with end time in local time Given the user activates Global Snooze for duration Sg When snooze is active Then no Rescue Ladder notifications are delivered for any habits until current_time + Sg Given snooze is active When the user taps Unsnooze Then snooze ends immediately and eligibility resumes with Next eligible send recalculated within 2 seconds Given snooze spans a device time zone change When computing the end Then the snooze ends at the same absolute instant and the displayed local end time updates accordingly
Respect Device Do Not Disturb and Calendar Busy
Given the app has permission to read device DND state When DND is active Then Rescue Ladder notifications are not delivered and are deferred to the end of the DND period, subject to caps and quiet hours Given the app has permission to read calendars and a busy or OOO event is active When the current time falls within the event Then Rescue Ladder notifications are not delivered and are deferred to the event end, subject to caps and quiet hours Given permissions are not granted for DND and/or calendars When evaluating sends Then normal scheduling applies without using those signals and no permission prompts are shown during scheduling Given a send is deferred due to DND or calendar busy When analytics is recorded Then notification_deferred is logged with reason dnd_active or calendar_busy and includes defer_until
Expose Effective Limits and Next Eligible Send in Settings
Given the user opens Rescue Ladder settings for a habit and global controls When data loads Then the UI displays per‑habit daily/weekly caps, global daily/weekly caps, remaining sends for today/this week, configured quiet hours, and whether DND/calendar signals are respected Given scheduling constraints change (cap reached, snooze toggled, DND/calendar state changes) When viewing settings Then Next eligible send updates within 2 seconds to the recalculated timestamp in local time and shows the top blocking reason Given accessibility is enabled When navigating the settings Then values are announced with labels and units (e.g., “3 remaining today”) and times use the user’s locale
Time-window Scheduling & Timezone Awareness
"As a routine-driven user, I want nudges aligned to my habit window and time zone so that reminders arrive when they’re relevant, even when I travel."
Description

Schedule the initial push near the end of the habit’s configured check-in window and stagger subsequent steps with configurable delays. Handle user time zone detection, DST transitions, and dynamic changes (e.g., travel) without duplicate or missed sequences. Support workday/weekend rules and per-habit windows. Provide backoff when a window has passed and the user already engaged. Integrates with habit schedules, a time service, and a resilient job scheduler.

Acceptance Criteria
Schedule Initial Push Near Window End (Per Habit, Local TZ)
Given a habit with a check-in window of 07:00–09:00 in the user’s local timezone and initial_push_offset = 5 minutes When the daily schedule is generated Then exactly one initial push is scheduled at 08:55 local time for that habit and user Given the computed initial push time is in the past but before the window end at the moment of scheduling When scheduling occurs after 08:55 but before 09:00 Then the push is scheduled immediately with a lead time ≥ 1 second and ≤ 30 seconds Given the computed initial push time would fall after the window end When the schedule is generated Then no initial push is scheduled for that window
Staggered Escalations with Configurable Delays and Stop-on-Check-in
Given an initial push sent at t0, delay_step2 = 2 minutes, delay_step3 = 3 minutes, max_steps = 3, and fallback_channel = SMS When the user has not checked in by t0 + 2 minutes Then step 2 is sent at t0 + 2 minutes And when the user has not checked in by t0 + 5 minutes Then step 3 is sent at t0 + 5 minutes via SMS And no further steps are scheduled for that window Given the user checks in at any time after t0 and before any pending step fires When the check-in is recorded Then all pending Rescue Ladder jobs for that window are canceled within ≤ 5 seconds and no further messages are sent Given max_steps = 2 When scheduling the sequence Then only two steps (initial push + one escalation) are created
Timezone Change During Active Window (Dynamic Recalculation)
Given the user’s device timezone changes during an active habit window 07:00–09:00 local and an initial push was scheduled for (window_end − 5 minutes) but not yet sent When the timezone change event to a new local timezone is detected Then the system recalculates the initial push to new_local_window_end − 5 minutes And if now ≤ recalculated_time ≤ new_local_window_end, the push is scheduled immediately with ≤ 30 seconds delay And if now > new_local_window_end, the push is not sent And exactly one initial push is sent for that window and zero duplicates occur And any pending escalations are rescheduled relative to the actual send time of the previous step preserving configured delays
DST Transitions (Spring Forward/Fall Back) Without Duplicates
Given a DST start where local time jumps from 01:59 to 03:00 and a habit window of 01:00–03:00 with initial_push_offset = 5 minutes When scheduling the initial push at 02:55 local time (nonexistent) Then the system schedules the initial push at 03:00 (next valid minute) and escalations maintain their real elapsed delays from actual send time And exactly one initial push is sent Given a DST end where 01:00–02:00 repeats and a habit window of 01:00–03:00 with initial_push_offset = 5 minutes When executing the schedule across the repeated hour Then the initial push occurs once at the first 02:55 occurrence and a deduplication key prevents a second send during the repeated hour And each escalation sends at most once at the configured delays
Workday/Weekend Rules and Per-Habit Windows
Given a habit configured for workdays (Mon–Fri) only with a window 18:00–20:00 and initial_push_offset = 5 minutes When the current day is Saturday in the user’s local timezone Then no Rescue Ladder sequence is scheduled for Saturday And the next sequence for this habit is pre-scheduled for Monday at 19:55 local by 00:05 Monday local time Given a user has two habits with distinct windows and rules When generating schedules Then independent sequences are created per habit without conflict or cross-cancellation
Backoff When Window Passed and User Already Engaged
Given the user checked in at 18:10 within a window 18:00–20:00 When the scheduler evaluates the Rescue Ladder for that window Then no initial push or escalations are scheduled or sent for that window Given the user checks in after the initial push but before step 2 fires When the check-in is recorded Then step 2 and all subsequent jobs are canceled within ≤ 5 seconds and no further messages are sent Given the window has ended and the user checked in during the window When a delayed or retried job resumes Then the system applies backoff and drops the sequence for that past window
Resilient Job Scheduler and Idempotency
Given a process restart or job retry occurs during an active Rescue Ladder sequence When the job is re-enqueued Then idempotency keys ensure at-most-once delivery per step per window And on recovery, pending steps are restored from durable state with correct remaining delays And missed windows are not retro-delivered; only the next eligible window is scheduled Given the time service or scheduler is temporarily unavailable When scheduling fails Then the system retries with exponential backoff up to 3 attempts and logs a counter metric rescue.scheduler.failures incremented by 1 on final failure
Transparency, Logs & Effectiveness Analytics
"As a motivated user, I want to see how Rescue Ladder affected my streaks so that I can tune settings for maximum benefit."
Description

Offer an in-app activity log that shows which Rescue Ladder steps were sent, suppressed, or canceled and why. Provide metrics such as rescue rate, step-level effectiveness, time-to-check-in, opt-outs, and caps hit. Enable cohort analysis and A/B testing of message templates and delays to optimize saves without added nag. Apply privacy safeguards with minimal data retention and allow export for support. Integrates with analytics pipeline, BI dashboards, and settings UI.

Acceptance Criteria
Event Logging for Rescue Ladder Steps and Reasons
- Given Rescue Ladder is enabled and a sequence is scheduled, When a step is evaluated, Then an immutable event is written with fields: correlation_id, attempt_id, user_id_hash, habit_id, step_type (push|watch_tap|email|slack|sms), step_order, status (sent|suppressed|canceled), reason_code, reason_detail, timestamp_utc (ISO 8601), template_id, channel_destination_hash, and experiment_id (nullable). - Given the user checks in after any step, When the check-in event is received, Then all pending steps in the attempt are canceled within 1 second and a cancellation event is appended for each with reason_code=checked_in and no further steps are emitted. - Given daily cap, quiet hours, opt-out, or channel disabled conditions, When a step is suppressed, Then an event is appended with status=suppressed and reason_code in {cap_hit, quiet_hours, user_opt_out, channel_disabled, rate_limited, experiment_holdout}. - Given a delivery provider callback (success or failure), When the callback arrives, Then a delivery_update event is appended referencing correlation_id with delivery_status (delivered|failed) and provider_error_code (nullable) within 120 seconds of send.
In-App Activity Log Viewing and Filtering
- Given a user opens Settings > Rescue Ladder > Activity Log, When a date range within the last 30 days is selected, Then a timeline of Rescue Ladder attempts displays grouped by attempt_id with per-step rows showing status, reason_code, local timestamp, and template_id. - Given channel and status filters, When filters are applied (e.g., channel=email, status=suppressed), Then the list updates within 500 ms and only matching rows are shown. - Given a sequence is rescued by a check-in, When the attempt is viewed, Then a Saved badge is shown on the step after which the check-in occurred and no subsequent steps appear. - Given large histories, When the list exceeds 200 events, Then infinite scroll pagination loads additional pages with p95 page-load latency <=300 ms and maintains scroll position. - Given privacy rules, When viewing the log, Then user identifiers and channel destinations are masked (e.g., ****1234), message bodies are not shown, and an info tooltip explains reason_code values. - Given timezones, When the user changes device timezone, Then all timestamps render in local time with a UTC tooltip and no gaps/overlaps for DST transitions.
Effectiveness Metrics Computation and Freshness
- Given a reporting period is selected, When computing rescue rate, Then rescue_rate = (count of attempts with a check-in within 30 minutes after any step) / (count of attempts started in period), expressed as a percentage with two decimals. - Given step-level attribution, When computing step effectiveness for step k, Then step_k_effectiveness = (count of attempts where check-in occurs after step k and before step k+1 within 30 minutes) / (count of attempts that reached step k), per channel and template_id. - Given rescued attempts, When computing time-to-check-in, Then publish p50/p90 time deltas from first step sent to check-in in minutes. - Given user behavior, When tracking opt-outs and caps hit, Then expose daily counts and rates by channel: opt_outs_per_1k_users and caps_hit_per_1k_users. - Given data pipelines, When new events are produced, Then in-app metric tiles refresh within 5 minutes (p95) and warehouse aggregates are available by 07:00 UTC daily. - Given cross-source reconciliation, When comparing in-app and BI values over identical periods, Then relative difference for rescue_rate and step_k_effectiveness is <=1%.
Cohort Analysis and Analytics/BI Integration
- Given cohort selectors (platform, timezone bucket, tenure bucket: 0–7, 8–30, 31+ days; habit category; day-of-week), When a cohort is applied, Then all metrics recompute within 2 seconds and tooltips display cohort definitions. - Given event emission, When Rescue Ladder events are produced, Then they are delivered to the analytics pipeline with p99 end-to-end latency <=60 seconds to the warehouse table and conform to the documented schema version. - Given BI dashboards, When daily refresh completes by 07:00 UTC, Then dashboards expose cohort filters matching the app and metric values align within 1% relative. - Given data quality monitoring, When event volume or key metric deviation exceeds 5% against 7-day baseline, Then an alert is sent to the data quality channel with run IDs and impacted tables. - Given schema evolution, When fields are added, Then backward compatibility is maintained (no breaking changes) and a migration note is published before deployment.
A/B Testing of Message Templates and Delays with Guardrails
- Given an experiment with variants A and B, When users become eligible, Then users are randomly assigned per user_id_hash with configured allocation (default 50/50) and assignment remains sticky until experiment end. - Given experiment configuration, When variants set template_id and delay offsets, Then guardrails enforce: max 3 steps per user per day, min 5 minutes between steps, and quiet hours respected; violations block scheduling with a clear error. - Given experiment concurrency, When a user is enrolled in a Rescue Ladder experiment, Then they are excluded from other concurrent Rescue Ladder experiments. - Given an A/A health check with >=10,000 users, When allocation is evaluated, Then sample ratio mismatch absolute error <1% and relative error <3%. - Given ongoing results, When viewing the experiment report, Then show lift in rescue_rate, step_k_effectiveness, and median time-to-check-in with 95% CIs, and allow min sample size and max duration (<=14 days) with optional auto-stop on p<0.05.
Privacy Safeguards, Retention, and Settings Controls
- Given retention policy, When storing events, Then raw Rescue Ladder events are retained for 30 days and de-identified aggregates for 180 days, both configurable and audit logged. - Given data minimization, When logging, Then message bodies are never stored; only template_id and metadata; channel destinations are hashed with salt and masked in UI; user_id is hashed and not reversible. - Given user control, When a user toggles "Share Rescue Ladder analytics" to off, Then only operational counters required for caps are retained locally and no events are exported to the analytics pipeline for that user within 60 seconds. - Given a right-to-be-forgotten request, When processed, Then all related raw events and user-linkable aggregates are purged within 30 days and a verifier job confirms zero remaining rows. - Given access control, When a non-authorized actor attempts to view logs, Then access is denied and the attempt is recorded in an audit log.
Support Export with Redaction and Audit Trail
- Given a support case, When an agent requests an export for a user and date range (<=30 days), Then a CSV or JSON file is generated with documented columns, excluding message bodies and PII, and masking channel destinations. - Given security controls, When an export is requested, Then just-in-time approval is required; the download link expires in 24 hours; max 10,000 events per export; and rate limiting enforces <=5 exports per agent per hour. - Given performance constraints, When exporting 10,000 events, Then the export completes within 60 seconds and the event count matches the in-app log count within 1%. - Given governance, When an export occurs, Then an immutable audit record is created with requester_id, target_user_id_hash, date_range, file_type, timestamp, and reason.

Early Drift Alert

Detects midday drift versus your typical pattern and sends a friendly heads-up showing time left and your next best check-in. Creates a wider runway to act, reducing last-minute stress and preventing streak decay before it starts.

Requirements

Baseline Habit Pattern Modeling
"As a routine-focused user, I want the app to understand my usual check-in times so that alerts only trigger when I’m truly drifting from my normal pattern."
Description

Compute a per-user, per-habit baseline of typical check-in windows and cadence using recent history (e.g., last 14–28 active days, day-of-week aware) to determine expected midday progress and time-at-risk. Handle cold start with heuristic defaults until sufficient data accrues, and automatically adapt to time zone changes and DST. Persist baselines in a lightweight profile store updated daily and on significant behavior shifts. Expose a read API for the Drift Detection engine with confidence scores to minimize false positives.

Acceptance Criteria
Day-of-Week Aware Baseline from Recent Active Days
Given a user has between 14 and 28 active days for Habit H in the last 60 calendar days, When the baseline job runs, Then it computes for each day-of-week a typical check-in window based on the central 60% interval of check-in times for that day. Given any day-of-week has fewer than 3 active samples, When computing that day’s window, Then the system falls back to the global median window from the user’s other days. Given inactive days with no check-ins exist, When building the baseline, Then those days are excluded from sampling. Given the baseline computation completes, Then the profile record for Habit H contains windows for all 7 days-of-week and an expected cadence per day. Given baseline computation executes for a single user-habit, Then the per-user-per-habit compute time is ≤200 ms at P95.
Cold Start Heuristics and Graduation to Learned Baseline
Given a user has fewer than 7 active days for Habit H, When the baseline is requested, Then heuristic defaults are returned using the habit’s category and configured frequency. Given the user accrues at least 7 active days spanning at least 3 distinct days-of-week, When the next daily job runs, Then the baseline switches from heuristic to learned values with confidence ≥0.60. Given the baseline source changes from heuristic to learned, When persisting, Then the profile record includes source, version, and generatedAt timestamp. Given the learned baseline shows high volatility (interquartile range >120 minutes) for a day-of-week, When computing confidence, Then confidence for that day is capped at 0.50.
Time Zone and DST Adaptation
Given the device time zone changes by at least 60 minutes or a DST transition is detected, When the next baseline read or update occurs, Then all baseline windows are reprojected to the new local time and persisted within 10 minutes. Given a time zone change is detected, When evaluating drift for Habit H, Then drift alerts are suppressed until the next local midnight. Given check-ins occur within 2 hours around a DST shift, When training samples are ingested, Then local wall-clock times are used consistently post-shift.
Significant Behavior Shift Detection and Rebaseline
Given the rolling median check-in time for the last 5 active days differs from the learned baseline median by at least 90 minutes, When the daily job runs, Then a rebaseline is triggered and a new baseline is computed and persisted. Given a rebaseline occurs, When computing confidence, Then global confidence decays by 0.15 and increases by 0.05 per subsequent stable active day up to a maximum of 0.95. Given no significant shift is detected for 14 consecutive active days, When evaluating, Then the rebaseline flag is cleared. Given the user deviates for 2 or fewer active days, When assessing for shift, Then no rebaseline is triggered.
Profile Persistence and Read API Contract
Given a baseline is computed, When persisting, Then the profile store record includes userId, habitId, version, generatedAt, timeZone, windowsByDOW, cadenceByDOW, source, and confidence. Given the Drift Detection engine requests GET /v1/baselines/{userId}/{habitId} and a record exists, When serving the request, Then the API returns 200 with the persisted fields within 100 ms at P95. Given no baseline record exists, When the API is called, Then it returns 200 with heuristic defaults and confidence ≤0.40 and source=heuristic. Given the requester is unauthorized for the userId, When the API is called, Then it returns 403. Given the request includes If-None-Match matching the current ETag, When the API is called, Then it returns 304 without a body.
Confidence Score Calibration and False-Positive Control
Given historical labeled data from the last 90 days, When backtesting drift detection using the baseline confidence scores, Then the midday drift false-positive rate is ≤5% at confidence ≥0.70. Given the standard deviation of check-in times for a day-of-week exceeds 120 minutes, When computing confidence for that day, Then confidence is reduced proportionally relative to variance. Given the computed confidence for the current day-of-week is <0.50, When serving the baseline via the read API, Then the confidence value returned is <0.50 for that day and may be used by consumers to suppress alerts.
Daily and Event-Driven Update Triggers
Given a new local day starts at 00:00, When the daily baseline job runs, Then all eligible user-habits are evaluated and updated baselines are persisted by 04:00 local time. Given a significant behavior shift or time zone change event is emitted, When processing the event, Then a recompute for the affected user-habit is triggered within 5 minutes. Given the baseline compute job fails, When retry policy is applied, Then it retries up to 3 times with exponential backoff and logs a final failure metric. Given multiple recompute triggers arrive concurrently for the same user-habit, When persisting, Then only one new baseline version is stored (idempotent upsert).
Real-time Drift Detection
"As a busy professional, I want the app to detect when I’m falling behind my usual routine so that I can course-correct before I risk losing my streak."
Description

Continuously compare today’s in-progress behavior against the user’s baseline to identify midday drift with configurable thresholds and hysteresis. Evaluate at defined checkpoints (e.g., morning, midday, afternoon) and upon relevant events (missed window, inactivity). Suppress alerts if the user has already checked in or has an upcoming scheduled session within the safe window. Support per-habit sensitivity, day-of-week variations, and rate limiting to at most one drift alert per habit per day.

Acceptance Criteria
Drift detection vs baseline with threshold and hysteresis
Given a habit has an established baseline for expected check-in timing and progress And per-habit threshold (T) and hysteresis window (H) are configured or defaults are applied When an evaluation occurs and the user's current progress deviates from the baseline by >= T for the current checkpoint time slice And the deviation persists for at least duration H Then the system sets the habit's state to In Drift for today And if the deviation falls below T before H elapses, then the system does not set In Drift And if already In Drift, the state is maintained until the deviation stays < T for duration H to prevent flapping
Checkpoint and event-triggered evaluations execute
Given morning, midday, and afternoon checkpoints are configured for a habit When a checkpoint time is reached Then a drift evaluation is executed within 60 seconds of the scheduled time Given the system emits a relevant event for the habit (missed window or inactivity) When the event is received Then a drift evaluation is executed within 10 seconds of event receipt And evaluations do not run more frequently than necessary to satisfy hysteresis timing
Suppress drift alerts when check-in exists or safe window upcoming
Given a drift condition is detected for a habit today When the user has already completed a valid check-in for that habit today Then no drift alert is generated or sent Given a drift condition is detected for a habit When an upcoming scheduled session exists within the configured safe window for that habit Then no drift alert is generated during the safe window And after the safe window passes without a check-in, a subsequent evaluation may generate one alert (subject to rate limiting)
Per-habit sensitivity and day-of-week variations applied
Given multiple habits with different sensitivity thresholds and hysteresis values When each habit is evaluated Then the habit's own threshold and hysteresis values are used for drift determination Given the habit defines day-of-week variations in baseline behavior When today is evaluated Then the baseline for today's day-of-week is used And if a day-of-week baseline is unavailable, the general baseline is used as a fallback
One drift alert per habit per local day
Given no drift alert has been sent today for the habit and a drift condition is detected without suppression When an alert is to be sent Then exactly one drift alert is sent for that habit for the current local day and the send is recorded Given a drift alert has already been sent today for the habit When additional drift detections occur Then no additional drift alerts are sent today for that habit And the alert counter resets at the user's local midnight
Configuration changes take effect on next evaluation
Given a user updates the per-habit drift threshold and/or hysteresis settings When the next evaluation occurs for that habit Then the new settings are applied to the drift determination And any cached prior settings are invalidated within 60 seconds or before the next scheduled evaluation, whichever comes first
Friendly Heads-up Notifications
"As a user who values gentle nudges, I want a considerate alert with time left and a clear action so that I can act quickly without feeling spammed."
Description

Deliver a single, timely heads-up via push and in-app banner that includes remaining time before the at-risk window closes and a clear CTA to take action. Respect user preferences, quiet hours, OS-level focus modes, and localization. Deep link to the specific habit’s check-in flow or live room, and provide a fallback in-app surface if push is disabled. Support A/B testing of copy and cadence, and enforce daily/weekly rate limits to avoid notification fatigue.

Acceptance Criteria
Midday Drift Detected—Single Timely Heads-up Delivered
Given a midday drift is detected for habit H and the at-risk window has at least 30 minutes remaining And the user has push notifications enabled and is not rate limited When the system triggers the heads-up Then send exactly one push notification and render one in-app banner for H And the content includes the remaining time before the at-risk window closes And the banner and push each include a single clear CTA to take action now And the displayed remaining time is accurate within ±1 minute at the moment of render And no additional heads-up for H is sent that same day
Respect User Preferences, Quiet Hours, and Focus Modes
Given the user has disabled heads-up notifications for habit H or global app notifications Or the user’s quiet hours are active Or the OS focus/DND mode is active When a midday drift is detected for H Then do not deliver a push notification And queue a non-intrusive in-app banner to display on next app foreground within the validity window And do not render or schedule during quiet hours/focus if it would violate the user’s settings And log the suppression reason and next eligible time
Localized Content and Time Formatting
Given the device locale is L When the heads-up message is generated and rendered Then all text strings, numbers, and time formats are localized to L And if L is unsupported, fall back to English And time remaining uses local conventions (e.g., 28 min, 1 hr 05 min) And right-to-left languages render correctly with mirrored layout And the habit name and CTA are localized where translations exist
Deep Link to Specific Habit’s Check-in or Live Room
Given the user taps the CTA from the push or in-app banner for habit H When the app handles the deep link Then open directly to H’s check-in flow if check-in is currently allowed Else open H’s live room if active Else open H’s detail screen with a prominent actionable check-in button And navigation completes within 1 second after app foreground on reference devices And back navigation returns the user to the prior context without loops
Fallback In-App Surface When Push Is Disabled
Given the user has disabled push notifications at the OS or app level When a midday drift is detected for habit H Then do not attempt a push send And present an in-app banner on the next app foreground within 2 seconds And the banner contains the same core copy, localized time remaining, and CTA as the push And the banner remains visible until dismissed or the validity window expires And no duplicate banner appears once it has been dismissed for that detection
A/B Test Copy and Cadence with Holdout
Given experiment EarlyDriftHeadsUp_v1 is active with variants A, B, and Holdout at configured allocation When a user becomes eligible at drift detection time Then randomly assign the user to exactly one variant and persist assignment for the experiment duration And log exposure at assignment time with user, habit, variant, and timestamp And apply variant-specific copy and send cadence rules within global constraints (quiet hours, rate limits) And include variant identifiers on impression, click, deep link open, and check-in completion events And disabling or pausing the experiment reverts users to control behavior within 5 minutes
Daily/Weekly Rate Limits and Deduplication
Given global rate limits are configured as max 1 heads-up per user per calendar day and max 4 per rolling 7-day window When multiple drift detections occur for a user within a day Then send only the first eligible heads-up and suppress the rest with reason rate_limited And never exceed 4 heads-ups in any rolling 7-day window And counters update atomically per send attempt and reset correctly at window boundaries And deduplicate alerts for the same detection event across push and in-app surfaces And record all sends and suppressions for audit with timestamps and reasons
Next Best Check-in Suggestion
"As a time-pressed user, I want a realistic next best step I can take now so that I can keep my streak without overcommitting."
Description

Generate context-aware, minimum-viable next actions that preserve the streak, such as a short-form check-in (e.g., 2-minute version), a snooze-to-start prompt (e.g., start in 15 minutes), or a direct join to an active micro-commitment room. Tailor suggestions to the remaining runway and the habit’s acceptable variants. Pre-populate the check-in flow for one-tap completion and record the chosen suggestion for effectiveness analysis.

Acceptance Criteria
Midday Drift Detected: Tailored Next Best Suggestions
Given a user has at least one active daily habit with defined acceptable variants and durations And the system has a learned typical check-in window for that habit And the current time has drifted outside the user's typical window but before the streak risk threshold And the remaining runway T minutes is computed When the Early Drift Alert is triggered for the habit Then the system generates 2–3 next best suggestions prioritized by feasibility within T And at least one suggestion is a minimum-viable action that preserves the streak And no suggestion violates variant rules or requires duration > T And each suggestion displays its expected duration/effort and the time left label And suggestions are ordered by highest likelihood of on-time completion
One-Tap Short-Form Check-in Completion
Given the user taps a short-form check-in suggestion When the check-in flow opens Then the habit, variant, duration, and any room context are pre-populated And a single confirmation tap completes the check-in And 95th percentile end-to-end completion time is <= 2 seconds on a good network And the streak updates immediately upon completion And a success confirmation appears within 1 second And no additional text entry is required
Snooze-to-Start Suggestion Within Safe Runway
Given the system calculates the snooze interval S (default 15 minutes) and the habit's minimum viable duration Dmin And the remaining runway T minutes is computed When generating suggestions Then a snooze-to-start in S minutes suggestion is shown only if T >= S + Dmin And the suggestion displays the exact start time and time left after snooze When the user accepts the snooze suggestion Then a reminder is scheduled for now + S with a deep link to the pre-populated check-in And the snooze is auto-canceled if the user checks in before the reminder fires And no snooze suggestion is presented if T < S + Dmin
Direct Join to Active Micro-Commitment Room
Given there is an active micro-commitment room relevant to the habit with capacity available When the system generates next best suggestions Then a direct join suggestion is included When the user taps the direct join suggestion Then the app deep-links into the room in <= 2 seconds at p95 on a good network And the user's check-in context is pre-populated for one-tap posting upon join And the suggestion is not shown if no qualifying room exists
Accurate Runway Calculation and Display
Given the system maintains the user's streak risk threshold and typical check-in pattern in the user's local timezone When the Early Drift Alert is shown Then the time left (runway T) is displayed in minutes with correct timezone handling and rounding (round down to nearest minute) And T decreases in real time at 1-minute intervals while the alert is visible And all suggested actions are feasible within T at the moment they are shown And no 'preserve streak' suggestion is displayed once T <= 0
Suggestion Selection Logging and Effectiveness Tracking
Given the system presents next best suggestions When any suggestion is rendered Then an impression event is logged with suggestionType, habitId, runway T, and timestamp When the user selects a suggestion Then a selection event is logged with suggestionType, variantId (if any), runway T, timestamp, and context (roomId or snooze interval) And the system records the outcome: check-in completed within 30 minutes (boolean) and completion timestamp And analytics records are available for querying within 15 minutes of events And events contain no raw PII beyond stable anonymized user identifiers And a dismissal event is logged if the user dismisses the alert without acting
Active Room Nudge
"As a social motivator, I want to see an active room I can join immediately so that I’m more likely to follow through with my check-in."
Description

Surface currently active micro-commitment rooms relevant to the at-risk habit, showing participant count and momentum indicators. Provide a one-tap Join Now CTA from the alert and in-app banner. If no rooms are active, recommend the next upcoming session and allow quick RSVP with reminder. Integrate with presence services and respect room privacy settings.

Acceptance Criteria
Surface Relevant Active Rooms
- Given an Early Drift Alert for habit H is active, when the user views the alert or in-app banner, then the list displays 1–5 currently active rooms that match habit H or its mapped category and displays 0 unrelated rooms. - And each room card displays participant_count (integer >= 1) and momentum_indicator (integer = number of check-ins in the last 10 minutes). - And momentum_indicator is computed using presence data with max staleness ≤ 30 seconds. - And rooms are ordered by descending momentum_indicator, then descending participant_count. - And the list renders in ≤ 500 ms after the banner becomes visible, assuming the presence API responds in ≤ 300 ms.
One-Tap Join From Alert And Banner
- Given a room card is displayed, when the user taps "Join Now" on the push alert deep link, then the app opens directly to the room and completes join in ≤ 2 seconds for public rooms without additional taps. - And when the user taps "Join Now" on the in-app banner, then the app navigates to the same room join state with the same latency target (≤ 2 seconds). - And deep-link navigation preserves room_id and habit_id context 100% of the time. - And join attempts that fail due to network error surface a retry option within ≤ 1 second without losing context.
Fallback to Upcoming Session With RSVP
- Given zero active rooms matching habit H, when the user views the alert or banner, then a single upcoming session card is shown with start_time (local), host_name, and an "RSVP" CTA. - And the selected session is the soonest scheduled within the next 24 hours that matches habit H or its mapped category. - And tapping "RSVP" succeeds in ≤ 500 ms, adds the user to the session attendee list, and schedules a reminder notification 5 minutes before start. - And a confirmation message "RSVP saved" appears within ≤ 1 second. - And if no upcoming sessions exist within 24 hours, then the banner states "No upcoming sessions" and does not render the CTA.
Room Privacy And Access Control Respect
- Private or invite-only rooms are not displayed unless the user is a member or has an active invite. - Rooms requiring host approval display "Request to Join" instead of "Join Now"; tapping sends a join_request and shows "Pending approval" within ≤ 1 second. - Rooms with waitlist enabled display "Join Waitlist"; tapping adds the user to the waitlist and confirms within ≤ 1 second. - Joining a restricted room via deep link never bypasses access controls; unauthorized joins are blocked with a non-destructive message within ≤ 1 second.
Presence Integration And Resilience
- Presence service is queried on alert generation and refreshed on banner view; data staleness at render time is ≤ 30 seconds. - If the presence API errors or times out (> 500 ms), the UI falls back to the upcoming session scenario and logs the error with a correlation_id; no crash occurs. - If cached presence data exists (age ≤ 5 minutes), it is used to render immediately and refreshes to live data within ≤ 2 seconds after a successful call.
Analytics And Observability For Nudge Actions
- Every "Join Now" tap emits an analytics event with user_id (hashed), habit_id, room_id, source ("push"|"banner"), and join_latency_ms. - Every "RSVP" tap emits an analytics event with user_id (hashed), habit_id, session_id, and scheduled_offset_minutes. - Analytics events include app_version and are sampled at 100%; validation passes against the event schema in CI. - 99% of events are delivered to the analytics backend within 5 seconds in the test environment; failures are retried up to 3 times.
Alert Outcome Learning & Tuning
"As a user who dislikes unnecessary pings, I want the app to learn from my behavior so that I only get alerts that help me act."
Description

Instrument alert delivery, views, actions, and outcomes (e.g., check-in within 30–60 minutes, streak saved) to compute per-user and per-habit effectiveness. Automatically adjust thresholds, suppress ineffective alerts, and refine suggestion strategies over time. Provide admin dashboards and experiment hooks for A/B tests while adhering to privacy and data retention policies. Expose a user-facing setting to opt out or adjust sensitivity.

Acceptance Criteria
Outcome Event Instrumentation and Effectiveness Computation
Given an Early Drift Alert is delivered to a user for a specific habit When the alert is delivered, viewed, dismissed, snoozed, or results in a check-in within 5–60 minutes Then the system logs events alert_delivered, alert_viewed, alert_action, and checkin_attribution with timestamps, anonymized user_id, habit_id, alert_id, experiment_id, and channel within 60 seconds of occurrence Given at least 30 alert exposures for a user–habit in a rolling 14-day window When the nightly aggregation job runs Then it computes open_rate, action_rate, 30m_checkin_rate, 60m_checkin_rate, streak_saved_count, and writes results to the metrics store keyed by user–habit–date with job status recorded Given the metrics store is populated When a single user–habit effectiveness query is executed by tuning or dashboard services Then the read latency is p95 ≤ 300 ms and p99 ≤ 800 ms
Automatic Threshold Tuning and Alert Suppression
Given effectiveness metrics for a user–habit over the past 14 days When 60m_checkin_rate is ≥ 15% higher than baseline and alerts_sent ≥ 5 Then the tuning job lowers the drift detection threshold sensitivity by one step and records the new threshold version with rationale Given effectiveness metrics for a user–habit over the past 14 days When 60m_checkin_rate is ≥ 15% lower than baseline for 3 consecutive days and alerts_sent ≥ 5 Then alerts for that user–habit are suppressed for 7 days, suppression_reason is logged, and the decision is auditable in the dashboard Given a suppression window has ended or effectiveness has recovered to within ±5% of baseline When nightly tuning runs Then alerts are re-enabled and thresholds are recalibrated to the user’s P80 productive check-in time window and versioned
Suggestion Strategy Refinement Based on Outcomes
Given multiple suggestion variants exist (e.g., timing, copy, nudge type) When a user becomes eligible for an Early Drift Alert Then a variant is selected by the strategy engine, and exposure is logged with variant_id and policy (explore/exploit) Given each variant has ≥ 200 exposures and ≥ 50 outcome events in the last 28 days When the analysis job runs nightly Then the highest 60m_checkin_rate variant with a ≥ 10% uplift over control and non-overlapping 95% CI is promoted for that user–habit; underperforming variants are demoted or paused Given no variant meets significance thresholds When the strategy engine is configured Then exploration rate remains ≥ 10% for that user–habit
Admin Dashboard for Alert Effectiveness
Given an authenticated admin with role Analyst or above When they open the Early Drift Alert dashboard Then they can filter by date range (up to 90 days), habit, cohort, platform, and experiment, and view KPIs: deliveries, views, actions, 30/60m check-ins, streak_saved_rate, suppressions, and threshold versions Given filters are applied for a 30-day range with up to 1M events When the dashboard loads Then p95 time-to-first-chart ≤ 2.5 seconds and p95 export-to-CSV (50k rows cap) ≤ 10 seconds Given a data point or user–habit row is clicked When drill-down is requested Then event-level records (with anonymized IDs) appear with pagination and no PII
A/B Experimentation Hooks and Exposure Logging
Given an experiment flag is active for Early Drift Alert When an eligible user triggers alert evaluation Then they are bucketed by consistent hashing into configured arms (default 50/50), and assignment is logged before any alert decision Given a user is enrolled in an experiment arm When they receive and interact with an alert Then all exposure and outcome events attribute to the assigned arm and appear in experiment reports with guardrails (DAU, opt-out rate) tracked Given a user has opted out or the user–habit is under suppression When experiment enrollment is evaluated Then the user is excluded from enrollment and excluded from experiment denominators
User Opt-out and Sensitivity Controls
Given a user opens Settings > Early Drift Alert When they toggle Opt-out ON Then future alerts and alert-related event logging stop within 60 seconds across all devices, a confirmation is shown, and an opt_out event is logged Given a user selects a sensitivity level (Low, Medium, High) When they save the setting Then the mapping to drift thresholds updates immediately, persists to their account, syncs across devices within 60 seconds, and is used by the next evaluation Given a user re-enables alerts after opting out When tuning resumes Then only post re-enable data is used for future effectiveness calculations for that user–habit
Privacy Compliance and Data Retention Enforcement
Given event logging for Early Drift Alert When an event is recorded Then no PII fields are stored; user identifiers are hashed; only minimal metadata (timestamps, habit_id, device_type, experiment_id) is kept Given the raw event retention policy is set to 90 days and aggregates to 12 months When the daily retention job runs Then raw events older than 90 days and aggregates older than 12 months are deleted, with an audit log entry created Given a user submits a data deletion request When the request is processed Then all raw and aggregate Early Drift Alert data for that user are purged within 7 days and the user is excluded from future analyses and experiments

Window Autopilot

Automatically adjusts your rolling streak window length between 20–28 hours day to day based on recent check-in times, sleep/wake patterns, and calendar shifts. Keeps your window aligned to real life so late nights or early starts don’t jeopardize your streak—no manual edits needed.

Requirements

Adaptive Window Computation Engine
"As a daily habit tracker, I want the app to automatically size my streak window to my real schedule so that late nights or early starts don’t break my streak."
Description

Implements the core algorithm that determines a user’s daily rolling streak window between 20–28 hours based on recent check-in cadence, predicted sleep/wake cycles, timezone context, and calendar shifts. The engine calculates a baseline from the median of the last 7–14 days’ inter-check-in intervals, applies weighted adjustments from detected late nights/early starts, and clamps results to the 20–28 hour bounds. It uses hysteresis to avoid oscillations, enforces a minimum step change (e.g., ±30–60 minutes/day), and locks the day’s window after the first check-in or at a configurable daily anchor time. Outputs include start/end timestamps and machine-readable reason codes for transparency. Runs on-device by default for privacy, with server validation to ensure consistency across rooms. Integrates with the streak validator so check-ins are evaluated against the computed window without user intervention.

Acceptance Criteria
Baseline Median Calculation and Clamping
Given the user has 10 inter-check-in intervals within the last 14 days with a median of 24 hours and no detected anomalies, When the engine computes today's rolling window at 08:00 local time, Then window_length is set to 24 hours and is clamped to the [20h, 28h] bounds if outside, And the output includes start_iso and end_iso whose absolute difference equals window_length within ±1 second, And timestamps are timezone-aware (IANA zone) with UTC-normalized equivalents provided.
Late/Early Pattern Weighted Adjustments
Given the baseline window_length is 24 hours and the last 3 check-ins occurred ≥90 minutes later than the median check-in time, When the engine computes today's window, Then it increases window_length by at least the configured minimum step and ensures the result remains within [20h, 28h], And the output includes an adjustment reason indicating a late_night pattern with the minutes applied. Given the baseline window_length is 24 hours and the last 3 check-ins occurred ≥90 minutes earlier than the median check-in time, When the engine computes today's window, Then it decreases window_length by at least the configured minimum step and ensures the result remains within [20h, 28h], And the output includes an adjustment reason indicating an early_start pattern with the minutes applied.
Hysteresis and Minimum Step Change Enforcement
Given day N window_length = 24h and inputs imply a +10 minute change on day N+1 with configured minimum_step = 30 minutes, When the engine computes day N+1, Then window_length remains 24h (no change). Given day N window_length = 24h and inputs imply a +90 minute change on day N+1 with minimum_step = 30 minutes and max_step_per_day = 60 minutes, When the engine computes day N+1, Then day N+1 window_length = 25h (increases by 60 minutes) and any remaining change is deferred to subsequent days subject to inputs. Given alternating late/early signals day-over-day within a ±20 minute band and hysteresis_threshold = 30 minutes, When the engine computes over 3 consecutive days, Then window_length does not oscillate more than once within the 3-day period and only changes when the aggregated signal exceeds the threshold.
Daily Window Locking at First Check-in or Anchor
Given today's window is computed at 06:00 and the first check-in occurs at 10:12 local, When the engine recomputes at 14:00 the same day, Then start_iso and end_iso are identical to the values established at 10:12 and remain unchanged until the next daily anchor. Given no check-in occurs by the daily anchor time of 05:00 local, When the engine reaches 05:00, Then it locks the day's window using the latest inputs as of 05:00 and subsequent recomputations return the same window until the next anchor. And locking is recorded in reason codes with lock_type (first_check_in|anchor) and locked_at timestamp.
Timezone and Calendar Shift (DST) Handling
Given a DST transition forward (+1h) occurs at 02:00 local on the computation day, When the engine computes windows that span the transition, Then the engine returns UTC timestamps that reflect the offset change and maintains the intended absolute duration in seconds (e.g., 24h remains 86,400 seconds). Given the user travels from UTC-5 to UTC+1 and the device timezone updates at 18:00 local, When computing the next day's window, Then the engine uses the new timezone context, applies adjustments/hysteresis as usual, and does not reset or invalidate the window solely due to the timezone change.
Reason Codes and Transparency Output
Given the engine computes a window, When producing the result, Then the output includes start_iso, end_iso, window_length_minutes, and reason_codes[] entries with machine-readable fields including: baseline_median_minutes, adjustments_applied_minutes (signed), hysteresis_applied (boolean), clamp_applied (boolean), lock_type (nullable), anchor_time_iso, timezone. And the sum baseline_median_minutes + adjustments_applied_minutes, after hysteresis and clamping, equals window_length_minutes. And each reason code entry includes a code string and numeric value where applicable.
On-Device/Server Consistency and Streak Validator Integration
Given on-device computation at 07:00 produces start_iso and end_iso, When the server validates using the same inputs, Then the server's start/end differ by no more than 60 seconds from the device result and both include a matching reason code checksum/hash. Given a user check-in timestamp T that lies strictly between start_iso and end_iso, When the streak validator evaluates the check-in, Then the check-in is marked valid for the current streak. Given a user check-in timestamp T that is at least 2 minutes before start_iso or at least 2 minutes after end_iso, When the streak validator evaluates the check-in, Then the check-in is marked invalid for the current streak.
Signal Ingestion & Sleep/Calendar Prediction
"As a privacy-conscious user, I want Window Autopilot to use my sleep and calendar patterns responsibly so that it can adjust my window accurately without exposing sensitive data."
Description

Aggregates and interprets signals needed for window sizing, including sleep/wake times (HealthKit/Google Fit/wearables), device usage heuristics (screen activity, Do Not Disturb/Focus), calendar context (late meetings, travel, shifts), and timezone changes. Provides permissioned, granular data access with clear consent, local caching, and fallback heuristics when integrations are unavailable. Produces a daily prediction of sleep/wake ranges and notable schedule shifts with confidence scores for the computation engine. Handles intermittent connectivity, debouncing frequent updates, and rate limits for external APIs. All processing follows data minimization, with raw health data staying on-device where possible.

Acceptance Criteria
Granular Consent & On-Device Processing
Given the user opens Window Autopilot Data Sources When they enable or disable Sleep/Wake, Device Heuristics, Calendar, or Timezone individually Then the OS permission prompt appears per source with clear purpose text, and the toggle reflects the final OS permission state Given a permission is denied or later revoked When Window Autopilot runs Then no raw data from that source is accessed or transmitted, a non-blocking notice explains reduced accuracy, and fallback heuristics are used without re-prompting in the same session Given raw health data is available When computing predictions Then processing occurs on-device and only derived aggregates (sleep_start, sleep_end, shift flags, confidence) are eligible for sync
Health Signals Ingestion, Debounce, and Rate-Limits
Given new sleep/wake samples arrive from HealthKit/Google Fit/wearables When the device is online Then ingestion completes within 10 minutes, duplicate samples are de-duplicated, and samples are timestamp-normalized to ISO 8601 with timezone Given multiple source updates arrive within 15 minutes When recomputing predictions Then recomputation is debounced to at most once every 30 minutes unless a critical change (>60 minutes shift in sleep_start or sleep_end) is detected, which triggers immediate recompute Given an external API responds with 429 or rate limit headers When retrying Then exponential backoff is applied (initial 2 minutes, doubling up to 60 minutes), honoring vendor-provided reset headers, and total retries do not exceed vendor policy per hour
Device Heuristics Fallback (DND/Focus and Screen Activity)
Given sleep data is missing for the prior night When DND/Focus and screen lock/unlock events are available Then the system infers a sleep range using the last unlock before quiet period and first unlock after quiet period, sets confidence <= 0.60, and tags sources_used = ["device_heuristics"] Given the user changes DND/Focus schedules When the quiet period shifts by >= 30 minutes Then the prediction is recomputed within 5 minutes and the rationale includes "focus_schedule_change"
Calendar Context & Timezone Changes
Given the user connects a calendar with consent When events classified as late meetings (end time after 22:00 local) or travel (flight/train keywords or timezone offsets) exist for the day Then a schedule_shift flag is set with rationale and the predicted window is shifted accordingly, with shift magnitude logged in minutes Given the device timezone changes When the OS posts a timezone change event Then detection occurs within 5 minutes, subsequent predictions use the new timezone, and the previous day’s prediction remains stored in its original timezone for integrity Given calendar becomes unavailable or offline When predictions are computed Then the last 7 days of cached events are used and delta sync completes within 10 minutes of connectivity restoration
Daily Prediction Output Contract & Quality
Given a new local day begins When Window Autopilot initializes before 04:00 local time Then a daily prediction object is available containing sleep_start, sleep_end, wake_range, schedule_shift_flags[], timezone, sources_used[], and confidence (0.00–1.00) Given ground-truth sleep is available for the prior day When evaluating prediction quality Then for predictions with confidence >= 0.70, the absolute error for sleep_start and sleep_end is <= 60 minutes in at least 80% of days over a rolling 14-day window Given no signals are available When generating a prediction Then a degraded_signals flag is set, confidence <= 0.30, and a heuristics-only prediction is produced
Local Caching, Security, and Data Minimization
Given signals or predictions are stored on-device When persisting to storage Then data is encrypted at rest using the platform keystore, raw source data is retained no longer than 30 days, and derived predictions are retained up to 365 days Given data is synced to backend When transmitting Then only derived prediction fields and minimal metadata (timestamps, source tags, confidence, shift rationale) are sent; no raw health samples or event bodies are transmitted Given the app restarts or the device reboots When resuming operation Then any queued ingestions and unsent predictions are restored from cache and processed within 5 minutes
Streak Integrity & Anti-Gaming Guardrails
"As a room participant, I want guardrails that keep streaks fair so that others can’t game flexible windows to gain an advantage."
Description

Defines and enforces rules that preserve fairness and prevent streak manipulation. Limits automatic adjustments to once per day, prohibits retroactive changes after a check-in is recorded, caps weekly cumulative extension (e.g., ≤12 hours), and never exceeds the 20–28 hour bounds. Detects suspicious patterns (e.g., repeated maximum extensions) and freezes Autopilot for review, reverting to a fixed 24-hour window with user notification. All adjustments are logged with timestamps and reason codes; server-side validation verifies eligibility before a check-in is counted. Guardrails are consistent across rooms so users cannot gain unfair advantages in micro-commitment sessions.

Acceptance Criteria
Daily Adjustment Rate Limit Across Rooms
Given a user with Window Autopilot enabled across multiple rooms When multiple Autopilot triggers occur within a rolling 24-hour period since the last applied adjustment for that user Then at most one window length adjustment is applied within that rolling 24-hour period per user across all rooms And any additional triggers within that period do not change the window length And server-side validation rejects any attempt to apply a second adjustment within the same rolling 24-hour period And the next eligible adjustment timestamp is recorded in the adjustment log
No Retroactive Window Changes Post Check-In
Given a check-in has been recorded at timestamp T within the current window When Autopilot evaluates a potential adjustment that would alter the window containing T Then the adjustment is not applied to any interval ending at or before T And the recorded check-in remains valid under the window definition in effect at T And any accepted adjustment applies only to windows starting after T And a suppressed adjustment is logged with reason_code = "blocked_due_to_recorded_checkin"
Weekly Cumulative Extension Cap Enforcement
Given a weekly cumulative extension cap of 12 hours per rolling 7-day period per user across all rooms When Autopilot computes a daily extension E_d = max(0, proposed_window_length_d − 24h) Then the sum of E_d over any rolling 7-day period does not exceed 12h And if the cap would be exceeded, the day’s extension is reduced so the rolling total equals exactly 12h And the system logs reason_code = "cap_truncated" with original and effective values And the user is notified of cap enforcement before their next check-in
Window Length Bounds Enforcement (20–28h)
Given Autopilot proposes a window length L for the next period When L < 20h or L > 28h Then the effective window length is clamped to the nearest bound within [20h, 28h] And server-side validation rejects any check-in evaluated against a window length outside [20h, 28h] And the adjustment log entry records proposed_length, effective_length, and reason_code = "out_of_bounds_clamped" And no client or room-level setting can override these bounds
Suspicious Pattern Detection and Autopilot Freeze
Given suspicious use patterns are monitored per user across all rooms When a user receives ≥3 maximum extensions (28h) within any rolling 5-day period OR ≥5 adjustments within 7 days that are clamped or cap-truncated Then Autopilot is frozen for that user for 7 days And the window reverts immediately to a fixed 24h across all rooms And the user is notified with reason_code and unfreeze date And a review ticket with the user’s adjustment summary is created for moderation And the freeze action is logged with decision_type = "freeze"
Server-Side Validation Before Counting Check-In
Given a check-in request is received When the server validates eligibility Then it recalculates the effective window using the latest eligible Autopilot state and guardrails (rate limit, bounds, weekly cap, freeze) And verifies the check-in timestamp falls within the effective window And if validation fails, the server returns 409 with a specific error code and does not count the check-in And if Autopilot is frozen, the server evaluates against a fixed 24h window and counts only if eligible And the validation outcome is recorded with request_id in logs
Adjustment Logging and Auditability
Given any Autopilot decision (applied, clamped, truncated, suppressed, or frozen) When the decision is made Then an immutable log entry is written with fields: user_id, room_id, decision_type, previous_length, proposed_length, effective_length, timestamp_utc, reason_code, trigger_source, prior_week_extension_total, next_eligible_adjustment_at, request_id And logs are retained for ≥180 days and are queryable via admin tooling And logs exclude raw sensitive inputs (e.g., raw sleep data), storing only derived signals/flags And each entry is included in admin export for audit
Transparency & Controls UI
"As a user, I want to see why my window changed and be able to turn Autopilot off per habit so that I feel in control and informed."
Description

Introduces UI surfaces that clearly explain and control Window Autopilot. Adds a Today’s Window module showing remaining time, end timestamp, and current length; a Why this window? explainer with signals used and reason codes; a per-habit Autopilot toggle (on/off) with immediate effect from the next window; and a change log listing recent daily adjustments. Provides a Next window preview range to set expectations and an educational tooltip when Autopilot first activates. Ensures accessibility (voiceover, color contrast) and parity across iOS/Android/web. All strings localizable.

Acceptance Criteria
Today’s Window Module: Accuracy, Refresh, and Edge Cases
- Given Autopilot is ON for a habit and a current window is active, when the user opens the app, then the Today’s Window module displays remaining time that matches the calculated time within ±60 seconds, the end timestamp in the user’s local timezone with correct DST handling, and the current window length in hours between 20.0 and 28.0 inclusive. - Given the app remains foregrounded, when one minute elapses, then the remaining time decrements without skipping or stalling. - Given the app is backgrounded for ≥1 minute, when it returns to foreground, then the remaining time snaps to the correct value within 1 second. - Given the window has expired, when the user views the module, then the module indicates “Window ended” and no remaining countdown is shown. - Given loss of connectivity, when server sync is unavailable, then a fallback client timer continues and values reconcile within 2 seconds after connectivity returns.
“Why this window?” Explainer: Signals and Reason Codes
- Given a user taps “Why this window?”, when the explainer opens, then it lists the signals used for the latest calculation with their current values or statuses and relative weights. - Given the latest window was adjusted, when the explainer opens, then a reason code from the approved taxonomy is shown with a plain-language explanation and the adjustment delta in minutes (e.g., +90). - Given any signal is unavailable, when the explainer opens, then that signal is labeled “Unavailable” and a fallback reason code “Baseline applied” is shown. - Given the explainer is opened, then it shows the calculation timestamp. - All explainer text is localizable and accessible to screen readers with logical focus order.
Per-Habit Autopilot Toggle: Behavior and Persistence
- Given a habit with Autopilot ON, when the user toggles Autopilot OFF, then the current window remains unchanged and the change takes effect at the next window boundary. - Given Autopilot is toggled OFF, when the next window starts, then the window length remains fixed during that window and does not adjust automatically. - Given a user toggles Autopilot ON, when the next window starts, then the window length becomes eligible to adjust within the 20–28 hour range. - Given the user changes the toggle on one platform, when the same habit is viewed on another platform, then the toggle state matches within 10 seconds or on next refresh. - Given app relaunch, then the last toggle state persists. - The toggle is fully operable via keyboard and screen reader and provides immediate UI feedback.
Change Log: Recent Daily Adjustments
- Given Autopilot has made adjustments in the past 7 days for a habit, when the user opens the change log, then it lists up to the last 7 entries in reverse chronological order. - Each entry shows date, previous window length, new window length, delta in minutes, end timestamp shift, and the reason code. - Given there were no adjustments in the past 7 days, when the user opens the log, then it shows “No recent adjustments”. - All timestamps are presented in the user’s local timezone and respect DST transitions. - Entries are consistent across iOS, Android, and Web for the same habit and are navigable by screen readers.
Next Window Preview Range
- Given Autopilot is ON, when the user views the Next window preview, then it displays an earliest and latest predicted end timestamp and a min–max length in hours for the next window. - Given the user completes today’s check-in, when Autopilot recalculates, then the preview range updates within 5 seconds or on next screen refresh. - Given Autopilot is OFF, when the user views the preview, then a single expected end timestamp (not a range) is shown. - Preview labels and time formats follow the device locale and are readable by screen readers.
First-Activation Educational Tooltip
- Given a habit enables Autopilot for the first time, when the user next views the habit screen, then a non-blocking tooltip appears explaining Autopilot and how windows adjust. - When the user dismisses the tooltip or after 10 seconds, then it does not reappear for that habit unless the user explicitly requests help. - The tooltip shows only once per habit across iOS, Android, and Web by syncing a “seen” flag. - The tooltip is keyboard focusable, readable by screen readers, and meets color contrast requirements. - All tooltip strings are localizable.
Accessibility, Localization, and Cross-Platform Parity
- All new UI elements expose role, name, and value to VoiceOver/TalkBack/ARIA; interactive controls support keyboard navigation and visible focus. - Color contrast for text and critical indicators meets WCAG 2.1 AA in light and dark modes; no information is conveyed by color alone. - Dynamic text scaling up to 200% does not cause truncation or overlap; responsive layouts adapt on Web. - All user-facing strings are externalized for localization; date/time/number formats respect device locale; English is used as fallback if translation is missing. - Parity: iOS, Android, and Web each expose Today’s Window, “Why this window?”, Autopilot toggle, Change log, Next window preview, and the first-activation tooltip with the same data points and behaviors.
Boundary-Aware Notifications
"As a busy professional, I want timely reminders before my streak window closes so that I can check in without last-minute panic."
Description

Delivers proactive reminders aligned to dynamic window boundaries. Schedules smart nudges at configurable offsets (e.g., T−60m, T−15m) before the window closes, auto-reschedules if the window length changes, and suppresses alerts during Focus/quiet hours with a catch-up summary after. Supports per-habit preferences, deep links to one-tap check-in, and local device scheduling for reliability offline. Includes guardrails to prevent notification spam when windows shift frequently.

Acceptance Criteria
Auto-Reschedule When Window Shifts
Given a habit with a rolling window close at 21:00 and offsets T-60m and T-15m enabled, When Window Autopilot shifts the close to 22:30 at 20:10, Then the previously scheduled notifications for 20:00 and 20:45 are canceled and rescheduled for 21:30 and 22:15 respectively. Given a window close shifts earlier from 22:00 to 21:10 at 20:30, When the new T-60m (20:10) is in the past, Then no immediate backfill fires and only the next valid upcoming offset (T-15m at 20:55) remains scheduled. Given multiple window shifts (>1) occur within 10 minutes, When rescheduling reminders, Then only the latest schedule is applied and no more than 1 reschedule operation occurs per habit per 5 minutes. Given a rescheduled reminder would duplicate an existing reminder (same habit, same offset, same window), Then the duplicate is not created (deduplication by habitId+windowId+offset). Given the user has disabled Boundary-Aware Notifications for a habit, When the window shifts, Then no reminders are scheduled or delivered for that habit.
Quiet Hours Suppression With Catch-Up Summary
Given device Focus/Do Not Disturb or app Quiet Hours are active from 22:00–07:00, When a T-60m or T-15m reminder would fire within that interval, Then the reminder is suppressed (no alert) and logged as deferred. Given Quiet Hours end at 07:00 and there are deferred reminders for still-open windows, Then within 5 minutes a single catch-up summary is delivered aggregating at most one reminder per habit with the earliest missed time, and no more than 1 summary is sent per device per 15 minutes. Given the window expired during Quiet Hours and the habit was not checked in, When the catch-up summary is sent, Then it indicates the missed window and does not present a check-in action. Given the window remains open at Quiet Hours end, When the catch-up summary is sent, Then it includes a deep link for one-tap check-in per referenced habit. Given OS-level Focus exceptions allow time-sensitive notifications, When enabled by the user, Then only the final pre-close reminder (closest offset) may bypass suppression; all others remain suppressed.
Per-Habit Reminder Preferences and Offsets
Given a habit’s notification settings allow selecting offsets from {T-120m, T-60m, T-30m, T-15m, T-5m} with defaults T-60m and T-15m, When the user saves changes, Then only the selected offsets are scheduled for that habit starting with the next computed window. Given the user toggles “Remind me for this habit” off, When windows open/shift, Then no Boundary-Aware Notifications are scheduled or delivered for that habit until re-enabled. Given the user sets Minimum Reminder Spacing to 30 minutes, When multiple selected offsets would violate spacing due to shifts, Then earlier conflicting reminders are dropped or moved to preserve ≥30 minutes between reminders. Given the user updates preferences on one device, When another device is online, Then updated preferences are applied to local scheduling within 60 seconds or on next app launch, whichever comes first.
Deep Link to One-Tap Check-In
Given a Boundary-Aware Notification for a habit arrives while its window is open, When the user taps the notification, Then the app opens directly to that habit and presents the one-tap check-in control in focus within 1 second. Given the notification includes a “Check in” action, When the user taps it while the window is open and the device is offline, Then the check-in is queued locally and the user receives a success confirmation in-app on next launch; the streak updates upon sync without user re-action. Given the window has closed by the time the notification is tapped, When the app opens, Then no check-in is performed and the habit detail shows “Window closed” state with no streak change. Given the deep link contains an invalid or stale habitId, When tapped, Then the app opens safely to the Home screen with an error toast and no check-in performed.
Local Device Scheduling and Offline Reliability
Given reminders are scheduled locally, When the device is offline or the app is terminated, Then reminders fire at the expected times with a delivery delay of ≤2 minutes in 99% of cases over a 7-day rolling measurement. Given the device time zone changes or DST shift occurs, When the window end and offsets are recomputed, Then reminders remain aligned to the new window close such that T-XXm still occurs XX minutes before the recalculated close. Given the device reboots, When the OS sends boot-complete, Then all future reminders for open and upcoming windows are restored locally within 60 seconds without user action. Given battery optimization restrictions are present, When scheduling, Then the app uses OS-approved exact or alarm APIs so that pre-close reminders are not deferred beyond 2 minutes; overruns are logged with reason codes.
Notification Spam Guardrails During Frequent Shifts
Given more than 3 window shifts occur for a habit within 30 minutes, When rescheduling reminders, Then no informational notifications about schedule changes are shown to the user and only the final pre-selected offsets remain scheduled. Given any combination of scheduled and rescheduled reminders for a habit in a 6-hour period, Then no more than 3 notifications are delivered to the user for that habit (hard cap), with earlier lower-priority offsets dropped first. Given two reminders for the same habit would be scheduled less than 10 minutes apart due to shifts, Then the earlier reminder is dropped to maintain minimum spacing. Given identical reminders (same habitId, windowId, offset) are created by concurrent processes, Then only one is kept via a stable deduplication key and idempotent scheduling.
Timezone, DST, Travel, and Offline Resilience
"As a frequent traveler, I want my streak window to behave predictably across timezones and DST so that my streak isn’t broken by travel or clocks changing."
Description

Ensures consistent behavior across time changes and connectivity states. Anchors daily windows in absolute time and converts to local time for display, handling DST transitions without accidental streak loss. On timezone jumps, preserves the current day’s window relative to origin timezone and recalculates at the next anchor. Records check-ins with monotonic device time for offline use, reconciling to wall-clock on sync while enforcing window eligibility. Defines clear rules for missed days, backfills (disallowed outside the window), and extreme shifts to maintain predictability.

Acceptance Criteria
DST Fall Back Within Active Window
Given a user in a timezone that ends DST on 2025-11-02 at 02:00 local (clocks roll back to 01:00) And an active streak window anchored to absolute time from 2025-11-02T06:00Z to 2025-11-03T10:00Z When the clock rolls back and the user checks in at 01:30 local after the rollback Then the check-in is accepted if and only if its absolute timestamp is within [start,end] And the streak remains unbroken And the timeline displays in local time with correct ordering across the repeated hour And no duplicate or overlapping windows are created
DST Spring Forward Skipped Hour
Given a user in a timezone that begins DST on 2025-03-09 at 02:00 local (skips to 03:00) And an active streak window anchored to absolute time spans across the transition When the user checks in at 03:15 local after the jump Then the check-in is evaluated against the absolute window and accepted only if within [start,end] And no streak is lost solely due to the missing local hour And the UI displays local times with the updated UTC offset after the transition
Mid-Window Travel Across Timezones
Given an active window anchored to absolute time [S,E] based on the origin timezone at anchor time And the user changes device timezone from UTC−08:00 to UTC+02:00 before E When the user attempts a check-in after the timezone change but before E Then the check-in is accepted if and only if its absolute timestamp is within [S,E] And the window end remains at E and does not re-anchor until the next window is created And at the next anchor, the new window is recalculated using the new timezone and recent behavior And no duplicate or overlapping windows are created during or after the change
Offline Check-ins Reconciliation Using Monotonic Time
Given the device is offline for up to 36 hours and the user performs one or more check-ins while offline And each offline check-in is recorded with monotonic device time When the device reconnects and syncs Then offline check-ins are reconciled to wall-clock timestamps preserving monotonic order And only reconciled timestamps falling within their respective active windows are accepted And reconciled timestamps outside windows are rejected with an "outside window" error And manual device clock changes while offline do not permit acceptance of any check-in that falls outside its window And the streak is updated based solely on accepted check-ins
Missed Window Backfill Disallowed and Streak Reset
Given the user misses an entire active window without a check-in When the subsequent check-in occurs in the next window Then the prior window is closed and the streak breaks according to product rules And no backfill option exists to add a check-in to the closed window And any attempt to submit a check-in timestamp earlier than the prior window end is rejected And the UI indicates the streak break at the prior window's end time
Extreme Timezone Shifts with Autopilot Bounds Enforcement
Given the user undergoes a timezone change of 10 or more hours within 24 hours or shifts schedule by 6 or more hours When Autopilot recalculates the next window at the next anchor Then the new rolling window length is constrained within 20–28 hours And the current active window remains unchanged until it ends And at no time are overlapping windows created And check-in eligibility during the shift is determined solely by absolute time within the active window
Metrics, Experimentation, and Rollout Controls
"As a product manager, I want robust metrics and controlled rollout so that we can validate Autopilot’s impact and iterate safely."
Description

Implements observability and safe rollout for Window Autopilot. Adds feature flags with kill-switch, staged percentage rollouts, and eligibility targeting. Captures KPIs (streak retention vs. control, adherence rate, check-in timing distribution, notification CTR) and system health (adjustment success rate, error rates, opt-out rate). Supports A/B tests comparing fixed 24h vs. Autopilot and alternative bands (e.g., 22–26h) and hysteresis settings. Provides dashboards and alerts for regressions, enabling data-driven tuning before full release.

Acceptance Criteria
Kill Switch Disables Window Autopilot Globally
Given the global kill-switch is toggled OFF in the feature flag console When any client session initializes or the server evaluates an adjustment job Then all Autopilot adjustment logic is bypassed for 100% of users within 5 minutes And an exposure event is emitted with variant="off" and reason="kill_switch". Given the kill-switch is OFF and adjustment jobs are queued When the scheduler runs Then pending Autopilot jobs are canceled within 5 minutes and no new jobs are enqueued. Given the kill-switch is OFF When a user’s window is next evaluated Then the system applies a fixed 24h window prospectively without altering historical streak records. Given the kill-switch is toggled ON When the next evaluation occurs Then Autopilot logic resumes using the last saved configuration.
Staged Percentage Rollout with Deterministic Bucketing and Eligibility Targeting
Given rollout is set to X% with hashing salt S and unit=user_id When the same user opens the app across devices Then the assignment is consistent across sessions and devices. Given rollout is increased from A% to B% When bucketing occurs Then only users beyond the original A% threshold are added to treatment and prior assignments are preserved. Given targeting rules (e.g., country=US AND platform=iOS) When evaluation runs Then only matching users are eligible for treatment; non-matching users remain in control and are logged as ineligible. Given any change to rollout percentage or targeting rules When published Then the change takes effect within 10 minutes and is recorded in an immutable change log with actor, timestamp, and diff. Given a user is evaluated When assignment occurs Then an exposure event is emitted with fields {experiment_id, variant, user_unit_hash, targeting_context, exposure_id, timestamp} and received server-side within 2 minutes; duplicate exposure_id events are deduplicated.
KPI and System Health Telemetry Capture and Quality Gates
Given a user completes a daily check-in When events are ingested Then KPIs are captured: adherence numerator/denominator, check_in_timestamp (ISO-8601 + timezone), and attributed notification impression/click for CTR; end-to-end availability within 5 minutes; schema validation failure rate < 0.1%. Given an Autopilot adjustment attempt When the system evaluates and applies a window Then an adjustment_success event records {proposed_window, applied_window, hysteresis, reason, success:boolean, error_code?}; daily success rate is computed and visible on dashboard. Given ingestion receives duplicate or late events When event_id duplicates occur Then duplicates are ignored; late arrivals up to 24h are accepted and processed in-order; events older than 72h are rejected and counted in a dead-letter metric. Given data governance rules When event payloads are validated Then no raw PII (email, phone) is present; user identifiers are hashed; build/deploy fails if new PII fields are introduced.
A/B Test: Fixed 24h Control vs Autopilot Treatment
Given experiment EXP-001 is configured with 50/50 split When 10,000+ eligible users are enrolled Then treatment/control imbalance is <= 1% and randomization passes a chi-square test (p>0.05). Given a user is assigned to a variant When the user reinstalls or uses a new device Then the assignment persists for 30 days or until experiment end via server-side bucketing. Given overlapping experiments target the same unit When assignment is evaluated Then mutual exclusion prevents co-assignment unless explicitly configured; conflicts are logged and surfaced on the experiment page. Given the experiment starts When dashboards refresh Then pre-registered primary metrics (28d streak retention, adherence rate, check-in variance) and guardrails (error rate, opt-out rate) are available with power and MDE estimates. Given the experiment is stopped When finalization runs Then exposure populations and results are snapshotted and downloadable (CSV/Parquet) within 30 minutes.
Alternative Bands and Hysteresis Configuration Experimentation
Given a multivariate experiment with variants {20–28h+H1, 22–26h+H2, Control} When users are treated Then each variant enforces its configured band and hysteresis consistently across evaluations and devices. Given hysteresis is configured When Autopilot evaluates within a 48h window Then window adjustments do not oscillate more than once for >=95% of treated users; violations are logged as oscillation_events. Given a configuration with invalid band (min>=max) or hysteresis < 0 When saving the configuration Then validation fails with actionable errors and no users are exposed to the invalid variant. Given band/hysteresis updates are published When the change propagates Then audit logs capture actor, before/after values, and rationale; propagation SLA <= 10 minutes.
Dashboards and Regression Alerts
Given observability dashboards are deployed When accessed via SSO by authorized roles Then p95 page load <= 3s, data freshness <= 15 minutes, and filters (variant, platform, cohort, timezone) apply correctly to all charts and exports. Given any guardrail regression is detected (>=2pp drop in 7d streak retention, >=20% increase in error rate, or >=5pp rise in opt-out rate vs baseline) When real-time or daily monitors run Then alerts are sent to Slack #autopilot-alerts and email within 10 minutes including link to runbook and top contributing cohorts. Given an alert is fired When acknowledged Then the system records ack user and timestamp; if not acknowledged within 30 minutes, it escalates to on-call with paging. Given a dashboard drill-down is performed When exporting data Then the exported CSV contains only the filtered subset and matches on-screen aggregates within 0.1%.

Jetlag Ramp

Smoothly transitions your rolling window after a timezone change by nudging it 1–3 hours per day until it matches local time. Prevents sudden window flips that cause misses, letting travelers maintain momentum while their body clock adjusts.

Requirements

Automatic Timezone Change Detection
"As a traveling remote worker, I want the app to recognize when my timezone changes so that my habit windows can adjust without manual setup."
Description

Automatically detects device timezone changes via OS signals and maps them to a new local time context without requiring location permission. On detection, records the offset delta, debounces rapid oscillations, and initiates a Jetlag Ramp proposal. Works cross-platform (iOS/Android/Web), supports DST transitions, and offers a manual override if signals are unavailable. Emits structured events for analytics and synchronizes the detected change and ramp state across devices so check-in validation and scheduling remain consistent.

Acceptance Criteria
OS Timezone Change Detected and Offset Recorded
Given the device timezone changes via the OS while StreakShare is running on iOS, Android, or Web When the app is in the foreground Then it detects the change and maps to the new local time context within 5 seconds without requesting or using location permissions And records previousTimezone, newTimezone, previousUtcOffsetMinutes, newUtcOffsetMinutes, and offsetDeltaMinutes Given the device timezone changes via the OS while the app is backgrounded When the app next resumes Then detection and mapping complete within 30 seconds of resume without requesting or using location permissions And a detection event is persisted with a correlationId
Debounce Rapid Timezone Oscillations
Given multiple timezone changes occur within a short period When changes occur within a 10-minute debounce window Then only the final stable timezone after 10 consecutive minutes is confirmed And at most one confirmed change event and one ramp proposal are produced per debounce window And suppressed transient changes are not applied to check-in validation
Jetlag Ramp Proposal Initiation
Given a confirmed timezone change with abs(offsetDeltaMinutes) >= 90 and not classified as DST When the change is confirmed Then a Jetlag Ramp proposal is created with dailyAdjustmentHours in [1,3] and a projectedCompletionDate that achieves full alignment And the proposal is surfaced to the user on next app open within 10 seconds of foregrounding And accepting the proposal activates the ramp and updates rolling window computation immediately And only one active ramp exists at a time
DST Transition Handling
Given an OS-reported DST change with abs(offsetDeltaMinutes) = 60 or a DST flag present When the change is confirmed Then the app updates the local time context immediately without initiating a Jetlag Ramp And no streak miss is recorded due solely to the DST adjustment And a DST-specific analytics event is emitted with dst=true
Manual Override Without Location
Given OS timezone signals are unavailable or were missed When the user manually selects a timezone in Settings Then previousTimezone, newTimezone, and offsetDeltaMinutes are recorded with source=manual And debounce and ramp rules identical to automatic detection are applied And the change can be reverted to the prior timezone within 10 minutes
Cross-Device Sync and Consistency
Given a confirmed timezone change or ramp activation on any device When the event is saved to the backend Then all signed-in devices reflect the updated local time context and ramp state within 10 seconds And check-in validation and scheduling use the synchronized state consistently across devices And repeated identical events are idempotent via correlationId to prevent duplicate ramps
Structured Analytics Event Emission
Given a confirmed detection, suppression by debounce, ramp proposal, ramp activation, manual override, or DST adjustment When the event occurs Then a structured analytics event is emitted containing: userIdHash, timestamp, previousTimezone, newTimezone, previousUtcOffsetMinutes, newUtcOffsetMinutes, offsetDeltaMinutes, source (os/manual/web), classification (dst/travel/debounced), correlationId, and ramp parameters (dailyAdjustmentHours, projectedCompletionDate if applicable) And events are delivered at-least-once to the analytics pipeline within 60 seconds or buffered for next connectivity
Gradual Window Shift Scheduler
"As a user crossing timezones, I want my check-in window to shift gradually each day so that I can maintain my streak without a disruptive schedule change."
Description

Implements the Jetlag Ramp engine that nudges each habit’s rolling check-in window by 1–3 hours per day until it aligns with the new local time. Calculates step size based on total offset and user preference, guarantees minimum viable window length, and prevents sudden flips. Precomputes the next ramp steps, applies them at defined cutover times, and integrates with the check-in validator to honor in-window events. Handles large offsets, overnight flights, and mid-ramp additional timezone shifts gracefully.

Acceptance Criteria
Ramp Plan Generation Based on Offset and User Preference
Given a detected timezone offset O (in hours) and a per-habit ramp preference P ∈ {1,2,3} hours/day When the scheduler generates the ramp plan Then it creates N = ceil(|O|/P) daily steps S_i with sign(S_i) = sign(O) And for every step i, 0 < |S_i| <= min(P, 3) And the sum of all S_i equals O exactly And the final step magnitude equals |O| - P*floor(|O|/P) (or 0 if |O| is divisible by P) And the plan is computed independently per habit
Precompute and Persist Upcoming Ramp Steps
Given a ramp plan exists for a habit When the engine requests upcoming steps or the app restarts Then the system returns the ordered list of remaining steps with absolute local cutover timestamps, step magnitudes, and resulting window boundaries up to full alignment And the plan is durably persisted and survives app restarts and backgrounding And any change to detected offset or preference regenerates the remaining plan within 1 second and replaces steps from the next cutover onward
Cutover Execution and In-Window Check-ins Honor
Given the next ramp step is scheduled at local cutover time C And the current window is [W_start, W_end] and the next window is [W’_start, W’_end] When local time < C Then the validator accepts check-ins only if timestamp ∈ [W_start, W_end] When local time >= C Then the validator accepts check-ins only if timestamp ∈ [W’_start, W’_end] And any check-in accepted before C remains accepted after C (no retroactive invalidation) And a check-in at exactly time C is evaluated against the post-cutover window [W’_start, W’_end]
Minimum Viable Window Length Preserved During Ramp
Given habit.minWindowMinutes = M When generating any window during the ramp Then the duration of each window is >= M minutes And if a planned step would reduce the next window below M, the scheduler clips or defers that step so the resulting duration remains >= M without exceeding the per-day step cap
Large Offset Handling Without Sudden Flips
Given a total timezone offset O with |O| > 0 and a preference P ∈ {1,2,3} When the ramp plan is generated Then each daily shift magnitude is <= 3 hours and <= P And the number of ramp days equals ceil(|O|/min(P,3)) And for each day during the ramp, a valid check-in window exists with duration >= habit.minWindowMinutes And no single day eliminates the window or flips it across an entire day
Mid-Ramp Additional Timezone Change Replan
Given a ramp is in progress with remaining offset R And a new timezone offset O2 is detected before alignment When the scheduler recalculates the plan Then all remaining steps are replaced by a new plan computed from O2 And the first new cutover is scheduled no earlier than the next defined cutover time And previously accepted or rejected check-ins retain their status (no retroactive changes)
Offline Travel and Delayed Sync Forward-Only Application
Given the device is offline across one or more timezone changes When connectivity resumes and the current local timezone is detected Then the scheduler computes the net offset from the last known anchor and generates a ramp from the next cutover forward And past cutovers are not applied retroactively; at most one step may execute on resume And check-ins recorded while offline are validated against the window active at their timestamps based on the last applied step, not on any skipped future steps
User-configurable Ramp Controls
"As a frequent traveler, I want control over how quickly and which habits ramp so that the adjustment matches my routine and obligations."
Description

Provides settings to opt in/out of Jetlag Ramp, choose ramp speed (1–3 hours/day), select per-habit vs. global application, and start, pause, skip a day, or cancel/revert a ramp. Allows locking specific habits to a home timezone. Exposes clear defaults and contextual prompts when a change is detected, with state synchronized to the backend for consistency across devices. Includes guardrails to prevent conflicting choices and shows immediate impact previews before applying changes.

Acceptance Criteria
Contextual Prompt on Timezone Change and Opt-In/Out
Given the device timezone offset differs from the user’s last known offset and the user opens the app, When the app loads the dashboard, Then a contextual Jetlag Ramp prompt appears within 3 seconds showing the feature summary, clearly labeled default settings (label contains the word "Default"), and actions: Start Ramp (primary), Not Now (secondary), and Learn More. Given the prompt is shown, When the user taps Not Now, Then no ramp state is created, no habit windows are shifted, and the prompt is snoozed for 24 hours unless another timezone change occurs. Given Jetlag Ramp is toggled Off in Settings, When the user confirms, Then any active ramp is canceled and no further shifts occur. Given Jetlag Ramp is toggled On in Settings without a detected offset difference, When the user attempts to start a ramp, Then the Start action is disabled with an inline message indicating no timezone change detected.
Ramp Speed Selection and Impact Preview
Given the user is configuring Jetlag Ramp, When selecting a ramp speed, Then only 1, 2, or 3 hours/day options are selectable and the system default is preselected and labeled as Default. Given a ramp speed is selected, When the preview renders, Then it displays: current offset delta in hours, daily shift amount, total ramp days = ceil(|delta|/speed), and projected completion local date. Given the user changes the speed, When the preview updates, Then all values recalculate immediately (<300 ms) before any Apply/Start action is taken. Given the user taps Apply/Start, When the configuration is saved, Then the selected speed is persisted and the next shift uses that speed on the next scheduled ramp tick (within 24 hours).
Per-Habit vs Global Application and Home-Timezone Lock
Given the user is configuring scope, When Global is selected, Then the ramp applies to all habits not locked to a home timezone. Given the user switches to Per-Habit, When selecting habits, Then only the selected, unlocked habits are included and the selection control supports multi-select. Given one or more habits are locked to a home timezone, When viewing the preview, Then those habits are marked as Locked and show no ramp-induced shift. Given all habits are locked or none are selected in Per-Habit mode, When attempting to Apply/Start, Then the action is disabled with an inline message indicating no eligible habits. Given a habit is locked, When the user attempts to include it in the ramp, Then the system prevents inclusion and surfaces a non-blocking explanation.
Start, Pause, Skip Day, and Cancel/Revert Controls
Given a ramp is configured and Start is confirmed, When the ramp begins, Then ramp state is created with start timestamp, initial offset delta, selected scope, and speed. Given a ramp is active, When the user taps Pause, Then no further daily shifts are applied while paused and existing shifted windows remain unchanged. Given a ramp is paused, When the user taps Resume, Then daily shifts continue using the current speed on the next scheduled ramp tick. Given a ramp is active, When the user taps Skip Day, Then the next shift is deferred by 24 hours without adding extra shift and the projected completion date recalculates accordingly. Given a ramp is active, When the user taps Cancel/Revert and confirms, Then all ramp-shifted habit windows revert immediately to the pre-ramp schedule, ramp state is cleared, and the preview returns to baseline. Given any control action completes, When success occurs, Then the user sees a confirmation toast and the action is recorded in activity logs with timestamp and actor device.
Guardrails for Conflicting Choices
Given no timezone delta is detected, When the user tries to start a ramp, Then Start is disabled and an inline reason is shown. Given a ramp is already active, When the user attempts to start another ramp, Then the action is blocked with a message to pause or cancel the current ramp first. Given the scope is Global, When the user attempts to lock all habits to home timezone, Then Apply/Save is blocked or the system auto-switches to Per-Habit with an explanation and no data loss. Given a habit is locked to home timezone, When the user tries to include it in the ramp selection, Then the selection control is disabled for that habit with a tooltip explaining why. Given the user changes ramp speed or scope, When there are unsaved changes, Then Apply is enabled and leaves are guarded by a confirmation to prevent accidental loss of pending edits.
Cross-Device State Synchronization
Given any ramp-related setting or control action is confirmed on Device A, When the backend acknowledges the write, Then Device B reflecting the same account shows the updated state within 10 seconds. Given Device A is offline, When the user makes ramp changes, Then the changes are queued locally and synced within 10 seconds after connectivity is restored, preserving action order. Given conflicting edits occur within 30 seconds across devices, When both reach the backend, Then last-write-wins by server timestamp is applied and both devices converge to the same state within 10 seconds. Given a ramp shift is executed by the scheduler, When devices refresh, Then no duplicate shift is applied and shift counters remain idempotent (exactly-once). Given a sync error occurs, When retry policy executes, Then the system retries with exponential backoff up to 3 attempts and surfaces a non-blocking toast if the final attempt fails.
Streak Safeguards and Fairness Rules
"As a competitive user, I want fair streak rules during time changes so that no one gains an advantage and my streak isn’t unfairly lost while traveling."
Description

Defines and enforces rules that protect streak integrity and prevent abuse during ramps: one check-in per habit per day; overlapping-day grace allowing either the pre-ramp or ramped window, with a cap on effective window length; DST-aware adjustments; and audit logging for dispute resolution. Updates backend validation and leaderboard logic to treat ramp days consistently, preserving fairness in social rooms and challenges.

Acceptance Criteria
Single Check-In Per Habit Per Local Day (Ramp and Non-Ramp)
Given a habit with an active Jetlag Ramp schedule toward timezone T When the user submits the first check-in within the allowed window for the local date in T Then the check-in is accepted and attributed to that local date Given the same habit and local date When the user attempts any additional check-in within the allowed window Then the request is rejected with HTTP 409 CONFLICT and error code DUPLICATE_DAILY_CHECKIN And the streak count, on-time status, and leaderboard points do not change
Overlapping-Day Grace and Effective Window Cap (Non-DST Days)
Given a ramp day with delta d in {1,2,3} hours and no DST transition When validating a check-in Then the allowed window is the union of [pre_ramp_start, pre_ramp_end] and [ramp_start, ramp_end] for that local date And the effective window length must be <= 24 + d hours (maximum 27h) And a timestamp outside this union is rejected with HTTP 422 UNPROCESSABLE_ENTITY and error code OUT_OF_WINDOW
DST-Aware Window Adjustments During Ramp
Given a ramp day that coincides with a DST forward shift (-1h) When validating a check-in Then the effective window cap is <= 23 + d hours (maximum 26h) And window boundaries align to local wall-clock times after the DST jump And non-existent local times are handled by mapping to the next valid instant Given a ramp day that coincides with a DST backward shift (+1h) When validating a check-in Then the effective window cap is <= 25 + d hours (maximum 28h) And ambiguous local times are disambiguated using timezone offsets (first occurrence preferred) And validation is performed on absolute instants in timezone T
Leaderboard and Challenge Consistency on Ramp Days
Given a valid accepted check-in on a ramp day When updating streaks, leaderboards, and challenge tallies Then exactly one daily credit is awarded for that local date And the participant’s position updates once based on the accepted check-in instant normalized to the user’s local date in T And no additional credit is granted for having a longer allowed window And all room members view the same credited date for the user, regardless of their own timezone
Validation Responses and Idempotency
Given an API client provides an Idempotency-Key header When the same request is retried within 24 hours Then the service returns the original result (success or error) without creating a duplicate record Given a duplicate daily check-in attempt When it is detected Then the service returns HTTP 409 CONFLICT with error code DUPLICATE_DAILY_CHECKIN Given a timestamp outside the allowed window When it is detected Then the service returns HTTP 422 UNPROCESSABLE_ENTITY with error code OUT_OF_WINDOW Given a valid first check-in When it is processed Then the service returns HTTP 200 OK with the resolved credited_local_date, window_matched in {pre_ramp, ramp}, and ramp_day flag
Audit Logging for Dispute Resolution
Given any check-in attempt (accepted or rejected) When validation completes Then an immutable audit record is written with fields: user_id, habit_id, credited_local_date, timezone T, device_timezone, pre_ramp_start/end, ramp_start/end, ramp_delta_hours, dst_shift in {-1,0,+1}, request_id, idempotency_key, received_at, client_event_time, server_decision in {accepted,rejected}, reason_code, validator_version And audit records are retained for at least 180 days And moderators with appropriate role can retrieve audit records by user_id, habit_id, date, or request_id within 60 seconds And audit records cannot be edited; corrections are appended as new records with a supersedes reference
Ramp-aware Notifications and Nudges
"As a busy creator, I want reminders aligned to my temporary ramp window so that I check in at the right time while adjusting to travel."
Description

Reschedules push notifications and local reminders to align with the current ramp step and upcoming cutover, including concise educational copy about remaining shift hours. Avoids duplicate alerts during midnight crossings, honors user quiet hours, and gracefully updates while offline using local scheduling with server reconciliation. Exposes in-app banners when a ramp starts or changes, with deep links to controls.

Acceptance Criteria
Ramp Step–Aligned Notification Rescheduling
Given a user has a daily reminder at 20:00 pre-ramp and a ramp step of +2h is active When the app computes today's schedule Then the reminder is scheduled at 22:00 local time for that day Given the ramp step changes (e.g., +2h to +3h) When the step becomes effective Then future reminders within the next 48 hours are rescheduled within 60 seconds to reflect the new step Given an upcoming cutover day When computing the next 24 hours Then exactly one reminder is scheduled within the user's rolling window and none outside it Given the device reboots When the app relaunches Then all ramp-aware reminders for the next 72 hours are restored within 30 seconds
Midnight Crossing Deduplication
Given a ramp shift yields candidate times at 23:30 and 00:30 within the same rolling window day When both times are evaluated Then only one alert is scheduled and only the later time fires Given both a server push and a local notification exist for the same reminder When the firing window arrives Then only one alert is delivered using a shared idempotency key with no duplicates Given delivery analytics are enabled When the reminder fires Then exactly one delivery event with that idempotency key is recorded
Quiet Hours Deferral and Compliance
Given user's quiet hours are 22:00–07:00 local When a ramp-adjusted reminder falls within quiet hours Then the alert is deferred to 07:00 unless that lies outside the rolling window, in which case it is scheduled at the last minute inside the window before quiet hours Given a deferment occurs When the alert is delivered Then the notification payload includes a quiet-hours-deferred flag in metadata Given multiple reminders are deferred to 07:00 When 07:00 arrives Then reminders are delivered at a minimum spacing of 2 minutes, preserving original priority order
Educational Copy with Remaining Shift Hours
Given a ramp is active with 3 hours remaining shift When a reminder is delivered Then the notification body includes a localized line indicating "3h left to align" and the total message length is ≤90 characters Given remaining shift is <60 minutes When a reminder is delivered Then the copy uses minutes (e.g., "45m left to align") rounded down to the nearest 5 minutes Given the remaining shift reaches 0 at cutover When the next reminder is delivered Then the educational line is replaced with "Aligned to local time" and no shift value is shown Given device language is English or Spanish When the reminder is delivered Then the educational line is localized with correct pluralization
Offline Local Scheduling with Server Reconciliation
Given the device is offline and a ramp step change occurs on the server When the app remains offline Then the client schedules the next 3 days of reminders locally based on the last known ramp and timezone Given the device reconnects When the app receives the new ramp step Then it reconciles within 60 seconds by rescheduling upcoming reminders and cancelling obsolete ones without firing duplicates Given a reminder fired while offline When reconciliation occurs Then no retroactive duplicate push is sent for that timeslot and the delivery is marked reconciled Given device clock skew is up to ±5 minutes When reconciliation occurs Then scheduled times are corrected to server time basis on the next cycle
In-app Banner for Ramp Start and Step Change
Given a ramp starts When the user opens the app Then a banner appears at the top of Home within 5 seconds showing title, concise explanation with remaining shift hours, and a deep link to Ramp Controls Given a ramp step changes while the app is foregrounded When the change event is received Then the banner updates its copy within 5 seconds without creating a duplicate banner Given the user dismisses the banner When they reopen the app during the same ramp state Then the banner stays dismissed for 24 hours or until the next step change, whichever occurs first Given the user taps the deep link When the controls screen opens Then focus lands on notification scheduling controls and the user can navigate back to Home
DST and Timezone Shift Robustness
Given a DST forward jump of +1 hour occurs during a ramp When computing that day's schedule Then a single reminder is placed relative to local wall time with the ramp step respected, with no skipped or duplicated alerts Given a timezone change of +8 hours occurs while ramp is active When the user opens the app after landing Then the ramp target aligns to the new timezone and the next reminder is computed from the current step within 60 seconds Given UTC offsets from −12 to +14 When generating schedules in tests Then all reminders fall within the rolling window and pass the no-duplicate-at-midnight rule
Visual Ramp Timeline in Habit View
"As a visual planner, I want to see how my windows will shift over the next few days so that I can schedule check-ins confidently while traveling."
Description

Adds a visual timeline showing today’s local check-in window, the previous window, and scheduled daily shifts until alignment. Includes a progress meter of hours remaining to full alignment, an explicit “Ramping” state label, color-coded windows, accessibility-friendly contrasts, and tooltips. Provides per-habit visibility with a global summary banner, and offers quick actions to snooze, accelerate, or exit the ramp.

Acceptance Criteria
Timeline Shows Today and Previous Check-in Windows
Given a habit with an active Jetlag Ramp and defined previous and today local windows When the user opens the habit’s Habit View timeline Then the timeline renders two distinct, labeled segments: “Previous Window” and “Today’s Window” And each segment displays start and end times in the device’s locale and time format (12/24h) And each segment shows the corresponding timezone abbreviation (e.g., PDT, JST) And the two segments use different, predefined colors and a non-color indicator (pattern/label) to differentiate them And the displayed times match the back-end ramp schedule within ±1 minute And the timeline renders within 500 ms on baseline devices
Timeline Shows Scheduled Daily Shifts Until Alignment
Given the ramp has not yet reached alignment When the user expands or scrolls the timeline to upcoming days Then the timeline shows a marker for each day until alignment, labeled with date and planned shift delta (e.g., +2h) And no single day’s planned shift exceeds 3 hours nor is below 1 hour unless the final remaining alignment is <1 hour And the number of markers equals the computed schedule to alignment based on the configured daily step size And the estimated alignment date/time (ETA) is displayed And any change to step size or quick actions (snooze, accelerate, exit) updates markers and ETA within 1 second
Progress Meter Displays Hours Remaining
Given an active ramp When the habit view loads or the ramp schedule changes Then a progress meter displays numeric hours remaining to full alignment with precision to the nearest 5 minutes And the meter shows proportional fill representing completion percentage And when remaining time ≤ 0 minutes, the meter reads “Aligned” and shows 100% completion And the meter updates within 1 second of any quick action or schedule update And the value matches the back-end remaining calculation within ±5 minutes
Ramping State Label and Tooltip Visibility
Given a habit is currently in a ramping state When the user views the Habit View header Then a visible “Ramping” label/chip appears adjacent to the habit schedule And activating the info icon or focusing the label shows a tooltip that explains the ramp, states today’s delta vs. target, and the ETA to alignment And the tooltip opens on tap/click or keyboard focus and dismisses on Escape, outside tap, or focus loss And the tooltip content reflects the current schedule and updates within 1 second after any quick action
Accessible Color Contrast and Non-Color Indicators
Rule: All timeline text and interactive elements meet WCAG 2.1 AA contrast (≥4.5:1 for normal text, ≥3:1 for large text/icons) Rule: Window differentiation is not reliant on color alone; patterns, labels, or icons are present for each window type Rule: All tooltips and quick actions are reachable via keyboard with a visible focus indicator (contrast ≥3:1) Rule: Screen reader roles and names are provided: timeline segments labeled “Previous Window” and “Today’s Window”; quick actions named “Snooze ramp”, “Accelerate ramp”, and “Exit ramp” Rule: Users can increase font size up to 200% without loss of information or functionality in the timeline component
Global Ramp Summary Banner Across Habits
Given the user has one or more habits in an active ramp When the user opens the Home or All Habits view Then a global banner displays the count of ramping habits and the earliest alignment ETA And tapping the banner navigates to a filtered list of ramping habits or opens ramp management for the earliest-ETA habit And dismissing the banner hides it for the current session and it reappears only if ramp state changes (new ramp starts or counts change) And when there are zero ramping habits, the banner is not shown
Quick Actions: Snooze, Accelerate, Exit Ramp
Given a habit with an active ramp When the user opens the timeline quick actions Then “Snooze”, “Accelerate”, and “Exit Ramp” are visible and enabled (subject to per-action limits) And selecting “Snooze” defers the next scheduled shift by one calendar day (max one snooze per habit per 24 hours) and updates the schedule, markers, and ETA within 1 second And selecting “Accelerate” increases the next scheduled daily shift by +1 hour up to a maximum of 3 hours for that day and updates the schedule, markers, progress meter, and ETA within 1 second And selecting “Exit Ramp” prompts a confirmation; upon confirm, the ramp ends immediately, windows align to the local schedule, future ramp shifts are cleared, and the UI updates within 1 second And each action presents an undo option for 10 seconds that restores the prior schedule exactly

Auto Timezone Sync

Instantly detects device timezone changes and offers a one-tap align-to-local-time for your rolling window. Preserves streak integrity across flights and layovers with clear previews and undo, so you stay protected without fiddling with settings.

Requirements

Instant Timezone Detection
"As a traveling user, I want the app to recognize when my timezone changes so that my habit windows reflect my current location without manual settings."
Description

Detects device timezone changes in real time using OS-level signals and resilient fallbacks. Listens to significant time change and timezone change callbacks where available, with a periodic UTC-offset verification fallback when callbacks are unavailable or restricted. Debounces rapid changes, persists the detected offset with timestamp, and triggers the sync flow only when the effective local day boundary for any active habit would be impacted. Operates offline by staging the change event and reconciling with the server on next connectivity, minimizing user friction and guaranteeing timely awareness of shifts due to flights or layovers.

Acceptance Criteria
OS-Level Timezone Change Detected
Given the OS broadcasts a timezone or significant time change event And the new UTC offset differs from the last persisted offset When the event is received by the detection service (foreground or background) Then the service captures the new IANA timezone and UTC offset and persists an event record within 1 second in foreground or within 3 seconds of background execution start And the record includes previous_offset, new_offset, source=os_signal, and detection_timestamp (UTC) And duplicate records for the same effective offset within a 5-minute window are suppressed
Fallback UTC-Offset Verification
Given OS callbacks are unavailable, restricted, or not delivered And the device UTC offset has changed by at least 15 minutes When the periodic UTC-offset verification runs Then the change is detected and persisted within 60 seconds while in foreground or within 10 minutes while in background And the record includes source=fallback and detection_timestamp (UTC) And if an identical offset has already been persisted in the last 5 minutes, no duplicate record is created
Debounce Rapid Successive Timezone Changes
Given multiple timezone/offset changes occur within a 30-second window When the detection logic processes these changes Then only the last observed offset after a 30-second quiet period is persisted And only one downstream sync-flow trigger is emitted for that persisted change And intermediate offset changes within the debounce window are ignored
Persist Offset and Timestamp Across Restarts
Given a timezone/offset change has been detected and persisted When the app is terminated and relaunched (cold start) Then the last known UTC offset, IANA timezone, and detection_timestamp are restored from storage And the values reflect the most recent persisted change event And the write operation is atomic such that a crash during write never yields partial or corrupt data on next launch
Trigger Sync Only When Habit Day Boundary Impacted
Given an offset change has been detected and persisted When evaluating all active habits' rolling windows anchored to local day Then the sync flow is triggered if and only if the UTC start or end timestamp of the current rolling window for any active habit shifts by at least 1 minute due to the new offset And no sync flow is triggered if no active habit meets this condition And at most one sync trigger is emitted per persisted change event
Offline Staging and Server Reconciliation
Given the device is offline when an offset change is detected When the change is persisted Then the event is marked staged and no server call is attempted And upon connectivity restoration, the staged event is sent within 10 seconds and marked delivered only after a 2xx acknowledgment And multiple staged events are sent in chronological order with idempotency so that only the latest offset remains active server-side And only a single sync trigger is emitted corresponding to the final effective offset
Ignore Manual Clock Changes Without Offset Change
Given the device clock time changes but the UTC offset remains the same When the detection logic evaluates incoming significant-time-change events Then no timezone/offset change record is persisted And no sync flow is triggered
One-Tap Align-to-Local Prompt
"As a user landing in a new timezone, I want a simple prompt to align my habit schedule so that I can continue checking in without confusion."
Description

Presents a non-blocking, accessible banner or modal immediately after a detected timezone change, offering a single action to align all eligible habit windows to the new local time. Summarizes impact with a concise preview of old vs new offset and the next check-in window, and provides secondary options such as keeping the previous timezone for a limited period or opening a help article for details. Honors theming, localization, and notification settings, and uses smart dismissal to avoid nagging while ensuring users can easily act in the moment.

Acceptance Criteria
Immediate Prompt After Timezone Change
Given the device timezone changes while the app is in the foreground, When the change is detected, Then a non-blocking banner or modal appears within 3 seconds of detection. Given the device timezone changes while the app is in the background, When the user returns the app to the foreground, Then the prompt appears within 3 seconds of app resume. Then the underlying screen remains interactive and scrollable, And the prompt is dismissible via a close control or tap outside (if modal). And the prompt is announced once by screen readers with a clear role and title, and initial focus does not steal from the current task unless the user opens the modal.
One-Tap Align Updates Eligible Habit Windows
Given a timezone change has been detected and eligible habits with rolling windows exist, When the user taps “Align to local time”, Then all eligible habit windows are recalculated to the new local timezone with no overlaps or gaps and are persisted to storage. And ineligible habits (e.g., fixed-UTC or locked schedules) are excluded from changes and are explicitly counted in the prompt’s preview. And check-in reminders are re-scheduled to the new local times without duplicating or retroactively firing notifications. And the alignment operation completes within 500 ms for up to 200 habits on a mid-range device. And an Undo affordance is shown for 30 seconds; When Undo is tapped within that window, Then all changes (windows and reminders) revert to the exact pre-alignment state.
Impact Preview: Old vs New Offset and Next Check-in Window
Given the prompt is shown, Then the UI displays old vs new timezone abbreviations and UTC offsets (e.g., PST UTC−8 → JST UTC+9), the relative delta (e.g., +17h), and the next check-in window start/end in local time. And the preview shows the count of affected habits and provides a Details link to per-habit times. When the user confirms alignment, Then the resulting schedule matches the preview exactly for each affected habit. And all dates/times and abbreviations are formatted per the device locale.
Secondary Option: Keep Previous Timezone Temporarily
Given the prompt is shown after a timezone change, When the user selects “Keep previous timezone”, Then the app retains the previous timezone context for all eligible habits for 24 hours. And a countdown indicator communicates the remaining keep period, and the user can align at any time during the period with one tap. And during the keep period, the align prompt is not shown again unless the user explicitly opens the align action. And streak evaluation uses the kept timezone during the period so that no streak loss occurs due solely to the timezone change.
Secondary Option: Open Help Article
Given the prompt is shown, When the user taps “Learn more”, Then an in-app webview opens the localized help article within 2 seconds and provides back navigation to return to the app context. And the help link has an accessible name describing its destination, and it is reachable via keyboard/switch control. And when the user returns from the article, Then the prior prompt state (dismissed or pending) is preserved without re-triggering the prompt.
Accessibility, Theming, and Localization Compliance
Given the prompt is shown, Then it adheres to the active theme (light/dark/high-contrast), meets WCAG 2.1 AA contrast, and respects reduced motion and dynamic text size settings. And all copy is localized to the device locale with English fallback when a translation is missing; date/time formats follow locale conventions. And the primary and secondary actions are reachable via one tap or keyboard activation; focus order is logical; screen readers announce action labels and the preview summary. And no system notification is generated for the prompt; any re-scheduled reminders retain the user’s existing notification channels, sounds, and quiet-hour settings.
Smart Dismissal and Non-Nagging Behavior
Given the user dismisses the prompt without acting, Then it is suppressed for 12 hours or until another timezone change is detected, whichever comes first. If the user completes alignment, Then the prompt is not shown again for that detected change. If the user chooses to keep the previous timezone temporarily, Then the prompt remains suppressed until the keep period ends. And the user can access the same align action later via Settings > Timezone & Scheduling and a contextual Inbox card.
Rolling Window Recalculation
"As a habit-focused user, I want my rolling windows to be recalculated correctly after a timezone change so that my streaks stay accurate and fair."
Description

Recomputes rolling check-in windows and streak counters when a timezone alignment is applied. Uses UTC as the canonical timeline and derives new local boundaries from the updated offset per habit cadence (daily, multiple-per-day, weekly). Ensures idempotent transformations, prevents double-counting or missed windows, and applies grace rules for near-midnight crossings. Preserves historical events in UTC with derived local-time metadata for audits, and emits analytics signals while updating caches to keep feeds, streak badges, and room states consistent.

Acceptance Criteria
Daily Habit: Timezone Alignment Across Midnight with Grace
Given a user has a daily habit with a rolling window tied to local-day boundaries and at least one UTC check-in in the last 24 hours And the current local timezone offset is -0500 and the new detected offset is +0100 When the user applies timezone alignment Then all daily window start/end boundaries are recomputed from immutable UTC events using the new +0100 offset And any check-in that crosses a local midnight boundary by <= 60 minutes due solely to the offset change is assigned to the prior local-day window (grace applied) And the user’s streak count remains unchanged from pre-alignment And no additional check-ins are created and none are dropped And no window overlaps exist after recomputation
Multiple-Per-Day Habit: Recompute Windows Without Double-Counting
Given a habit configured for 3 check-ins per local day with N UTC check-ins over the last 3 local days And each UTC check-in is assigned to exactly one sub-window in the pre-alignment state When timezone alignment from offset A to offset B is applied Then sub-window boundaries for each affected local day are recalculated using offset B And each existing UTC check-in maps to exactly one sub-window (no duplicates and no unassigned events) And the per-day completed sub-window count after alignment equals the pre-alignment count for each affected day And the streak count for consecutive days with all required sub-windows completed remains unchanged
Weekly Habit: Local Week Boundary Shift and Streak Preservation
Given a weekly habit with the local week starting Monday 00:00 and existing UTC check-ins spanning a week boundary When alignment changes the local offset such that some check-ins move across a local week boundary Then weekly window boundaries are recomputed as Monday 00:00 to Sunday 23:59:59 local in the new offset And any check-in moving across the boundary by <= 60 minutes is retained in the original-effective week (grace applied) And the weekly streak value remains unchanged And no partial weeks are created or dropped
Idempotency and Repeated Alignment/Undo
Given a pending timezone alignment to target offset B with idempotency key K When the alignment operation is executed more than once with the same parameters and key K Then the second and subsequent executions perform no additional mutations to events, windows, counters, caches, or badges And the post-alignment state hash equals the first execution’s state hash When the user triggers Undo for this alignment Then the pre-alignment state (events, windows, counters, caches, badges) is restored exactly And re-applying the same alignment after Undo yields the same post-alignment state hash as the first application
Audit Trail: UTC Preservation and Derived Local Metadata Update
Given historical UTC events exist for the habit and an alignment is applied from offset A to offset B Then the UTC timestamps of all events remain unchanged And derived local-time metadata for each event is updated to reflect offset B: local_date, local_time, tz_offset_minutes, local_week_start And an audit record is appended capturing: alignment_id, actor, timestamp, offset_before=A, offset_after=B, affected_event_count, grace_applied_count And audit records are retrievable via the /audit/alignment/{alignment_id} endpoint and include before/after derived local-time snapshots And referential integrity is preserved (no orphaned or duplicated event IDs)
Consistency and Cache Refresh Across Surfaces
Given a timezone alignment is applied Then in-app streak badges reflect recomputed counts within 2 seconds at p95 and 5 seconds at p99 And habit detail and room state surfaces display the new rolling window boundaries and countdown using the new offset within the same latency SLO And feed cards and scheduled notifications are updated to the new boundaries and do not display stale pre-alignment data after 5 seconds And no client presents conflicting states (e.g., differing streak counts) across surfaces during or after the consistency window
Analytics Emission for Recalculation
Given a timezone alignment is applied with parameters user_id, habit_id, cadence, offset_before, offset_after When the operation completes Then an analytics event timezone_alignment_applied is emitted exactly once with properties: alignment_id, user_id, habit_id, cadence, offset_before, offset_after, affected_days, windows_recalculated, checkins_reassigned, grace_applied_count, duration_ms, idempotency_key And if Undo is performed, an analytics event timezone_alignment_undo is emitted with properties: alignment_id, user_id, reason, duration_to_undo_ms And repeated execution with the same idempotency_key does not emit duplicate applied events
Preview and Undo Sync
"As a cautious user, I want to preview and undo timezone alignment so that I can avoid unintended streak changes."
Description

Provides a clear, compact preview of the impact before committing alignment, including today’s and upcoming window shifts and any streak risk. After apply, offers a reversible undo action within a limited window, with an immutable audit trail capturing previous and new offsets, actor, and reason. Handles edge cases gracefully, such as pending offline check-ins, overlapping device sessions, and partial synchronization, ensuring users can correct mistakes without contacting support.

Acceptance Criteria
Preview shows impact of timezone alignment
Given the device timezone changed within the last 24 hours and at least one habit uses a rolling window, When the Auto Timezone Sync prompt is opened, Then a preview displays current timezone, detected timezone, and proposed window start/end for today and the next 3 days. Given the preview is displayed, When any habit would be at risk of streak break due to alignment, Then the preview flags the habit(s) with risk type, count, and the earliest at-risk timestamp. Given the preview is displayed, When the user taps Apply, Then the applied alignment matches the previewed window boundaries and risk outcomes or blocks apply with a "State changed — refresh required" message if source data changed. Given the device is offline, When the preview is requested, Then the app shows a cached preview labeled "Estimated" and disables Apply until connectivity is restored and data is verified.
One-tap align with undo window
Given a successful alignment was applied, When the user remains within 10 minutes of the apply timestamp, Then an Undo action is visible and enabled across the sync banner and relevant screens. Given the Undo action is tapped within 10 minutes, When processing completes, Then all window offsets revert to the exact pre-apply state and any streak effects are restored. Given Undo is attempted after 10 minutes, When the user taps Undo, Then the action is disabled and a message states "Undo window expired". Given the app is force-closed and reopened within the 10-minute window, When the user returns, Then the Undo action remains available until the window expires.
Immutable audit trail for alignment and undo
Given an alignment or undo is applied, When the operation completes, Then an audit record is created with previous offset, new offset, actor (user id), reason (auto timezone sync), device id, request id, and server timestamp. Given an audit record exists, When a client attempts to modify or delete it, Then the operation is rejected and the original record remains unchanged. Given multiple devices perform operations, When audit records are retrieved by user id, Then they are returned in descending server timestamp order and include operation status (Success, Failed, Undone) and correlation to the opposing record (undoOf=request id) where applicable.
Pending offline check-ins preserved through alignment
Given there are pending offline check-ins from the last 48 hours, When the preview is displayed, Then it indicates the count of pending check-ins and whether alignment would change their window assignment. Given pending offline check-ins exist, When Apply is tapped, Then alignment does not discard pending check-ins; upon reconnection they are posted and mapped to the correct windows using their original local timestamps. Given alignment would assign a pending check-in across a window boundary, When connectivity is restored, Then the system maps the check-in based on its original local timestamp without breaking the streak.
Overlapping device sessions resolved safely
Given two devices have the sync preview open, When one device applies alignment, Then the other device's preview is invalidated within 5 seconds with a "State changed — refresh" notice. Given device A applies alignment and device B applies a different alignment within 10 seconds, When the server processes both, Then the later server-accepted operation wins and the earlier is marked "Superseded" in the audit with no partial state left on either device. Given a conflict is detected, When the loser operation is rejected, Then the app surfaces a non-blocking banner with a one-tap Refresh to fetch the latest alignment state.
Graceful handling of partial synchronization failures
Given an apply or undo operation starts, When a network or server error occurs mid-operation, Then the client rolls back to the pre-operation state and marks the attempt as Failed in the audit with error code. Given a partial failure is detected on a secondary device, When the device next comes online, Then it self-heals to the canonical server state within 60 seconds and clears any inconsistent UI banners. Given a failure occurred, When the user retries, Then a fresh preview is generated and the next apply proceeds without duplicating audit entries.
Cross-Device Timezone Consistency
"As a user with multiple devices, I want consistent timezone alignment across all devices so that my streaks and reminders stay in sync."
Description

Maintains a server-authoritative timezone state per account that reconciles inputs from multiple devices. Applies deterministic conflict resolution (e.g., most recent active session with verified offset, manual overrides take precedence) and introduces cooldowns and hysteresis to prevent oscillation when devices report different offsets. Propagates the chosen timezone to all sessions, re-syncs local schedules, and ensures check-ins are recorded against a consistent canonical timeline.

Acceptance Criteria
Deterministic Conflict Resolution – Most Recent Verified Session
Given an account with two or more active sessions reporting differing timezone offsets and verification flags When the server evaluates the canonical timezone Then it selects the offset from the most recent session with verified=true and last_activity within the past 15 minutes And if multiple verified sessions have identical recency, then the server selects deterministically by the lowest lexicographical session_id And if no verified sessions qualify, then the server retains the existing canonical timezone And the decision is idempotent: re-evaluating with the same inputs yields the same canonical timezone And the server persists an audit record including decided_offset, decided_at (UTC), source_session_id (or retained), and resolution_reason
Manual Override Precedence Across Devices
Given a user has set a manual timezone override for the account When any device reports a conflicting offset Then the canonical timezone remains the manual override value until the user clears or updates the override And the manual override propagates to all active sessions within 5 seconds And API responses include canonical_source=manual and override_set_at (UTC) And clearing the override immediately re-enables device-derived resolution rules on next evaluation
Cooldown and Hysteresis Prevent Oscillation
Given devices alternately report offsets that would cause frequent switching When the last canonical change occurred less than C=10 minutes ago Then the server suppresses further canonical timezone changes And a candidate offset must remain stable (no conflicting reports) for at least H=5 minutes and differ from the current canonical by at least D=30 minutes to be eligible And suppressed changes are logged with suppressed_change=true and include candidate_offset, first_seen_at, last_seen_at And once the candidate meets stability and delta thresholds and cooldown has elapsed, the canonical timezone updates exactly once
Cross-Device Propagation and Local Resync
Given the server has decided a new canonical timezone When the decision is persisted (version N) Then push notifications (or server-sent updates) are sent to all online sessions within 5 seconds And offline sessions receive the canonical timezone and version on reconnect before enabling check-ins And clients recompute local schedules and rolling windows using the canonical timezone within 2 seconds of receiving version N And updates are idempotent: clients ignore repeats of version N and only apply if received_version > current_version
Consistent Check-In Timeline Recording Against Canonical Timeline
Given a user performs a check-in from any device at time t (UTC) When the server processes the check-in Then the check-in is attributed to the rolling window computed from the canonical timezone effective at time t And the stored record includes utc_timestamp, canonical_timezone, canonical_version, and computed_local_time (display-only) And client-supplied local offsets are ignored for attribution And if the check-in falls outside the canonical rolling window, the server rejects with code ERR_OUTSIDE_WINDOW and returns current window bounds And subsequent canonical timezone changes do not retroactively re-bucket existing check-ins
Handling Unverified or Stale Timezone Reports
Given a session reports a timezone offset with verified=false or last_activity older than 15 minutes When the server evaluates the canonical timezone Then that session’s offset is excluded from consideration And if all reports are unverified or stale, the server retains the existing canonical timezone And a session is marked unverified if device clock skew exceeds 5 minutes versus server time or the OS timezone cannot be determined And all exclusions are recorded with reason=unverified or reason=stale
DST and International Date Line Transitions
Given a session’s OS changes offset due to DST or crossing time zones When the reported offset change magnitude is <= 2 hours within a 24-hour period Then the change is treated as a candidate subject to hysteresis H=5 minutes and cooldown C=10 minutes before updating the canonical timezone And when the change magnitude is >= 12 hours (e.g., date line crossing), the same hysteresis and cooldown apply, but the candidate is not ignored due to size alone And rolling windows that span the transition maintain continuity with no duplicate or missing windows And the audit log records transition_type (dst|travel), previous_offset, new_offset, and effective_at (UTC)
Smart Notification Rescheduling
"As a user who relies on reminders, I want notifications to adjust to my new local time so that I get timely prompts without spam."
Description

Automatically recalibrates reminder schedules, live room prompts, and nudges when timezone changes are detected or alignment choices are made. Preserves user intent windows (e.g., morning 7–9am) relative to the new local time, batches and spreads updates to avoid notification bursts, and respects platform Do Not Disturb and quiet hours. Updates pending notifications both locally and server-side to prevent duplicates and missed alerts.

Acceptance Criteria
Auto-Recalibration on Timezone Change
Given the device timezone changes by any offset When the OS timezone-change event is received or the app is foregrounded Then the scheduler recalculates times for all active reminders, live room prompts, and nudges to preserve their intent windows in the new local time And the recalculation completes within 3 seconds of event handling And no notification is dispatched during the recalculation window
Intent Window Preservation at Edges
Given a habit with an intent window (e.g., 07:00–09:00 local) and a reminder at 08:00 When the user selects Align to Local Time or an automatic alignment is applied after a timezone change Then the reminder is scheduled at 08:00 local in the new timezone on the next eligible day inside the 07:00–09:00 window And if current time is after the window end, the reminder shifts to 08:00 on the following day And if current time is before the window start, no catch-up reminder fires before 07:00 And if the reminder lands exactly at the window boundary, it fires at the boundary minute
Catch-up Batching and Throttling
Given rescheduling results in multiple notifications becoming due within the same 15-minute period When applying the new schedule Then the system enforces a throttle of maximum 1 notification per 60 seconds per user during catch-up And remaining notifications are spread evenly until the end of the 15-minute period or the next window boundary, whichever comes first And no two notifications are scheduled at the same second And the actual delivery order respects original priority: live room prompts > habit reminders > nudges
Quiet Hours and OS DND Compliance
Given OS Do Not Disturb or app quiet hours are active for a time range When a recalculated notification time falls inside that range Then no sound, vibration, or banner is produced during the quiet period And the notification is deferred to the first minute after quiet hours end or the start of the next intent window, whichever is later And the notification metadata records deferred_due_to_dnd = true
Local–Server Update Consistency (No Duplicates)
Given pending notifications exist both locally and server-side When a timezone change is detected or an alignment choice is made Then local and server schedules are updated to the same timestamps within 5 seconds And each logical notification maintains a single stable ID across stores And zero duplicate deliveries occur for the same ID within a 30-minute window And audit logs capture the reschedule operation with old_time, new_time, tz_before, tz_after
Offline Recalculation and Sync Reconciliation
Given the device experiences a timezone change while offline When the app next runs or regains connectivity Then local rescheduling executes immediately and queues a sync job And upon connectivity restoration, the server is reconciled within 10 seconds using last-write-wins by schedule_version And all reminders due after sync deliver inside their intended window boundaries, excluding deferrals due to quiet hours
Streak Integrity Safeguards
"As a fair-play user, I want safeguards around timezone changes so that streaks aren’t exploited or unfairly penalized."
Description

Implements fairness and anti-abuse controls tied to timezone changes. Enforces rate limits on alignment actions, disallows back-dated gains, and applies a one-time grace buffer to protect legitimate travelers from streak loss during long-haul transitions. Detects suspicious oscillations or repeated flips and pauses alignment pending confirmation, while surfacing clear rationale in the UI. Logs signals for trust-and-safety review without impacting normal users’ flow.

Acceptance Criteria
Rate Limit on Timezone Alignment Actions
Given a user has successfully aligned their rolling window within the last 24 hours, When the user attempts another alignment, Then the action is blocked and the UI displays the next available alignment time and a rate_limited reason code, and no state changes occur. Given a user has not aligned in the last 24 hours, When the user taps align to local time, Then the alignment succeeds, the attempt is counted toward the rate limit, and the event is logged. Given a user is rate-limited, When the 24-hour window elapses, Then the next alignment attempt succeeds without additional friction.
Disallow Back-Dated Streak Gains
Given a user aligns their rolling window to a new timezone, When the new window would include previously missed check-ins, Then those check-ins remain missed and streak count does not increase retroactively. Given alignment is previewed, When the preview is shown, Then it clearly indicates zero retroactive gains if any, before the user confirms. Given alignment is applied, When the system persists the change, Then check-in records retain immutable original timestamps and no historical entries are re-stamped. Given a user attempts to exploit undo/redo to create retroactive credit, When the user toggles alignment and undo, Then the system still prevents any back-dated gains.
One-Time Traveler Grace Buffer
Given the device timezone delta is >= 3 hours within a 24-hour period and the user's active streak deadline is within the next 3 hours, When Auto Timezone Sync detects the change, Then the user is offered a one-time 12-hour grace buffer with a preview and explicit opt-in. Given the user accepts the grace buffer and has not used it in the last 30 days, When applied, Then the streak deadline extends by 12 hours, the buffer token is marked consumed, and the event is logged. Given the user has already consumed the buffer in the last 30 days, When another qualifying timezone change occurs, Then no buffer is offered and the UI explains the reason. Given the grace buffer is applied, When the user taps undo within 5 minutes and has not completed a check-in within the extended window, Then the buffer is restored to unused state and original deadline is reinstated. Given suspicious activity is flagged for the account, When a qualifying timezone change occurs, Then the buffer is not offered and the UI surfaces a rationale.
Suspicious Timezone Oscillation Detection
Given more than two timezone changes of at least 1-hour offset occur within 2 hours, or the same two timezones are flipped between three or more times within 24 hours, When the user attempts alignment, Then the alignment is paused pending confirmation and a suspicious_activity reason code is shown. Given alignment is paused, When the user confirms they are traveling, Then the alignment proceeds subject to other safeguards and the confirmation is logged. Given alignment is paused, When the user cancels, Then no state changes occur and a cancellation reason is logged. Given oscillation is detected, When further timezone changes occur within the next 12 hours, Then subsequent alignment attempts continue to require confirmation. Given no further oscillations are detected for 24 hours, When the user attempts alignment, Then confirmation is no longer required.
UI Rationale and Accessibility for Blocked or Paused Alignments
Given an alignment is blocked or paused by safeguards, When the UI updates, Then an inline banner appears within 1 second stating the reason code (rate_limited, suspicious_activity, backdated_disallowed) and available next steps. Given the rationale banner is displayed, When a screen reader is active, Then the banner is announced with accessible labels and focus moves to actionable controls without trapping. Given the rationale banner is displayed, When the user taps Learn more, Then a help sheet opens explaining the safeguard and how to proceed. Given the rationale banner is displayed, When the blocking condition clears, Then the banner automatically dismisses on the next attempt and does not reappear.
Trust-and-Safety Signal Logging Without User Friction
Given a device timezone change or alignment attempt occurs, When the event is processed, Then a trust-and-safety signal is enqueued asynchronously with fields: user_id_hash, device_id, prev_tz, new_tz, delta_hours, ip_country, action, outcome, reason_code, timestamp. Given signals are enqueued, When network conditions are normal, Then 99.9% of signals are delivered within 60 seconds without adding more than 200 ms latency to the user action. Given network is offline, When events occur, Then signals are queued locally and retried with backoff until delivered or 7 days have passed, after which they are dropped. Given a normal user (<= 1 alignment/day and <= 2 timezone changes/week), When they align, Then no additional UI friction is introduced and the flow completes without extra prompts. Given privacy requirements, When logging occurs, Then raw IP addresses are not stored beyond coarse country code and all fields comply with the data retention policy.

Grace Edge

Adds a configurable 5–15 minute soft buffer at the end of your rolling window that still counts a check-in if you tap within it. Eliminates near-miss frustration while keeping accountability tight, perfect for back-to-back meetings.

Requirements

Room-Level Grace Window Configuration
"As a room host, I want to set a short grace buffer so that members with back-to-back meetings can still maintain their streaks without weakening accountability."
Description

Allow room hosts and moderators to enable Grace Edge per room and choose a buffer length between 5 and 15 minutes in 1-minute increments (default 10). The setting is surfaced in Room Info and pre-join screens, persists on the backend, and is versioned so mid-cycle changes apply starting with the next rolling window to prevent rule-shifts during an active session. Changes sync in near real time to all members and are included in room metadata for clients, exports, and invites. This integrates with room settings UI, backend models, and permissions, and ensures that all members operate under the same, clearly communicated grace policy.

Acceptance Criteria
Enable and Configure Grace Edge in Room Settings
- Given I am a room host or moderator on any supported client, When I open Room Info > Settings > Grace Edge, Then I can toggle Grace Edge on/off and select a buffer length between 5 and 15 minutes inclusive in 1-minute increments. - Given I attempt to set a value outside 5–15 or a non-integer, When I try to save, Then the control prevents the value and the Save action is disabled with an inline validation message indicating "Select 5–15 minutes". - Given I save a valid configuration, When the save succeeds, Then I see a confirmation and the settings view reflects the selected value. - Given a member opens the pre-join screen for the room, Then it displays the current Grace Edge state ("Off" or "X min").
Default Grace Buffer on Enablement and New Rooms
- Given I enable Grace Edge for a room (new or existing) and do not adjust the buffer, Then the buffer defaults to 10 minutes. - Given I create a new room and enable Grace Edge during creation, Then the buffer field is pre-populated with 10 minutes. - Given Grace Edge is enabled and the buffer is left blank via client input, When the request is saved, Then the backend persists 10 minutes as the buffer value.
Versioned Grace Changes Effective Next Rolling Window
- Given a rolling window is in progress, When a host changes the Grace Edge setting (toggle or buffer), Then the change is versioned and marked effective starting with the next rolling window, and the current window continues under the prior version. - Given the next rolling window begins, When members check in, Then the new grace configuration is applied to those check-ins. - Given multiple changes occur before the next window starts, Then only the most recent saved change becomes effective at the next window start. - Given a member views the settings after a change during an active window, Then the UI indicates the scheduled effective time (timestamp) and the current active version.
Near Real-Time Sync of Grace Policy to Members
- Given a host saves a valid Grace Edge change, When another member is connected to the room, Then the member’s client receives the updated grace metadata within 5 seconds. - Given a member is offline at the time of change, When they reconnect or open the room or pre-join screen, Then they fetch and display the latest grace metadata before interaction. - Given a change is scheduled for the next window, Then clients display an "Effective next window" indicator with the exact effective time. - Given a stale local cache exists, When the server responds with current metadata, Then the client reconciles to the server version and clears stale cache.
Permissions and Validation Enforcement
- Given I am not a host or moderator, When I attempt to access or save Grace Edge settings, Then the UI hides or disables the controls and backend returns 403 Forbidden for write attempts. - Given a host or moderator attempts to set a buffer outside 5–15 or a non-integer, When the request is sent, Then the backend rejects with 400 and a machine-readable error code (e.g., GRACE_BUFFER_OUT_OF_RANGE), and no change is persisted. - Given a host or moderator submits an enable/disable and buffer change together, Then the update is atomic: either both persist or neither does. - Given a network failure occurs during save, Then the client shows an error, rolls back the UI to the last confirmed server state, and no version increment occurs.
Grace Metadata in Room APIs, Exports, and Invites
- Given I query room metadata via API or client, Then I see fields: grace_edge_enabled (boolean), grace_buffer_minutes (integer), grace_version (integer), grace_effective_at (ISO-8601 timestamp or null). - Given I export room data (CSV/JSON), Then the export includes the same grace fields with values matching the backend at export time. - Given I generate or open a room invite link, Then the pre-join and invite surfaces display the current grace policy (and scheduled effective time if applicable); invites created before a change reflect the updated policy on open. - Given localization is enabled, Then the displayed grace policy text is localized while numeric values remain accurate.
Eligibility and Streak Attribution Logic
"As a member, I want taps within the grace period to still count so that a near-miss doesn’t break my streak."
Description

Make the server authoritative for check-in eligibility by accepting taps within [window_end, window_end + configured_grace] as valid and tagging them as grace events. Streak increments count grace-tagged check-ins as on-time while retaining the tag for transparency, analytics, and policy enforcement. Enforce one check-in per rolling window, prevent stacking or carryover across windows, and reject taps beyond the grace end with a clear reason. Handle edge cases including DST shifts, time zone changes, and room window adjustments without retroactively altering previously recorded outcomes.

Acceptance Criteria
Grace window tap accepted and tagged
Given a room with a rolling window ending at T_end and a configured grace G between 5 and 15 minutes And no check-in exists for the current window W When the server receives a check-in tap at time t where T_end <= t <= T_end + G Then the server marks the check-in as eligible for window W And tags the record with is_grace = true And returns HTTP 201 with payload including attribution.windowId = W and reason = "within_grace" And emits an analytics event checkin.recorded with is_grace = true And the response includes streakIncrement = 1
Single check-in per rolling window enforced
Given a successful check-in exists for window W When any subsequent tap is received with timestamp t where window_start <= t <= window_end + configured_grace Then the server does not create an additional check-in And returns HTTP 409 with code = "ALREADY_CHECKED_IN" and existingCheckinId And does not modify streak counts or analytics totals And does not carry the tap over to any other window
Taps beyond grace end rejected with clear reason
Given a room with configured_grace = G And a rolling window ending at T_end When the server receives a tap at time t where t > T_end + G Then the server rejects the tap And returns HTTP 422 with code = "BEYOND_GRACE" and reason = "outside_eligibility_window" And no check-in record is created And the response includes nextEligibleWindowStart and nextEligibleWindowEnd
Grace-attributed check-ins increment streaks while retaining tag
Given a check-in recorded for window W with is_grace = true When streak calculation runs for the user Then the grace check-in contributes 1 toward the streak the same as an on-time check-in And the record persists is_grace = true in storage and via read APIs And analytics exports include is_grace = true And UI/API surfaces expose the grace tag without altering streak values
DST and user timezone changes do not retroactively alter outcomes
Given the room schedule is defined in TZ_room and windows are computed server-side in UTC And the user device timezone changes during the day and/or a DST transition occurs across a window boundary When the server evaluates a tap at time t Then eligibility is determined using the server time and the room’s TZ_room schedule for the active window And previously recorded outcomes for earlier windows remain unchanged And the audit log captures the timezone and DST context used for evaluation
Room window or grace configuration changes apply prospectively only
Given an admin updates the room configuration (window duration and/or configured_grace) at time t_update And there is an active window W_active spanning t_update When the server evaluates taps after t_update Then the prior configuration applies for W_active And the new configuration takes effect starting with the next window W_next And previously recorded check-ins are not modified And the response includes effectiveConfigVersion and effectiveFromWindowId
Server-authoritative timing and idempotent submission
Given potential client clock skew and network latency When the server receives a tap with a clientProvidedTimestamp Then eligibility is determined using serverReceivedAt, not the clientProvidedTimestamp And two taps carrying the same idempotencyKey within the same window result in exactly one recorded check-in And concurrent taps for the same user and window produce exactly one record And subsequent retries after a successful check-in return HTTP 409 with existingCheckinId
Grace State UI Indicators
"As a user, I want a clear visual that I’m in the grace period so that I know I can still check in and how much time I have left."
Description

Provide clear, low-friction visuals for the end-of-window and grace states: countdown timer that shifts to an amber "Grace" state with seconds remaining, subtle "G" badge on grace-tagged check-ins in history, and room header text indicating the active policy (e.g., "10m Grace"). Include accessible labels and announcements for screen readers, support light/dark themes, and ensure the indicators are distinct from late or missed states to avoid confusion. Tooltips or tappable microcopy explain how the grace window works and when it applies.

Acceptance Criteria
Countdown Transitions to Amber Grace State
Given a room with a check-in window that ends at time T and Grace Edge set to G minutes (5–15) When the countdown reaches T Then the timer label switches to "Grace", the color changes to amber, and the remaining time displays in seconds updating every 1 second Given the timer is in Grace state When the countdown reaches T + G Then the timer exits Grace, the color changes to the non-grace state, and the label no longer includes "Grace"
G Badge Applied to Grace Check-ins in History
Given a user completes a check-in during the Grace window When viewing the History list and check-in detail Then a subtle "G" badge is displayed adjacent to the check-in time Given a check-in is on-time or late/missed When viewing History Then no "G" badge is displayed for that entry Given a check-in has a "G" badge When the app theme or surface varies (list vs detail) Then the badge remains visible and aligned consistently
Room Header Displays Active Grace Policy
Given Grace Edge is enabled with a configured value between 5 and 15 minutes When viewing the room header Then text of the form "<Xm Grace>" is visible and matches the configured value Given the Grace Edge value is changed by the user or admin When returning to or refreshing the room view Then the header updates to the new value within 2 seconds Given Grace Edge is disabled When viewing the room header Then no "Grace" policy text is displayed
Accessible Labels and Announcements for Grace States
Given a screen reader is enabled When the timer enters Grace Then an announcement states "Grace period started. <X minutes> remaining." Given a screen reader is enabled and the timer is focused When the timer is in Grace Then the accessible name includes the word "Grace" and the remaining time Given a screen reader is enabled When Grace ends Then an announcement states "Grace period ended" Given a grace-tagged check-in is focused in History When a screen reader reads its metadata Then the badge is announced as "Grace check-in"
Light/Dark Theme Contrast and Visual Distinction
Given the app is in light theme When the timer is in Grace Then timer text and indicators meet WCAG AA contrast ratio of at least 4.5:1 against the background Given the app is in dark theme When the timer is in Grace Then timer text and indicators meet WCAG AA contrast ratio of at least 4.5:1 against the background Given Grace and Late/Missed states are shown When evaluated visually Then Grace uses amber with the explicit label "Grace" and Late/Missed use non-amber colors with their explicit labels, making them distinguishable without relying solely on color
Grace Microcopy Tooltip or Tappable Help
Given the user taps or focuses the "Grace" label on the timer or the "G" badge in History When the help is triggered Then a tooltip or bottom sheet appears explaining that check-ins within <X minutes> after the window still count and when it applies, and provides a dismiss control Given keyboard or screen reader users activate the same target When the help opens Then focus moves into the help, it is readable, and can be dismissed without trapping focus (Esc/back/outside tap) Given the grace duration setting changes When reopening the help Then the copy reflects the current configured value
Grace Clearly Separate from Late/Missed States
Given a check-in occurs after the grace window ends When viewing History Then it is marked Late/Missed per existing rules and never shows a "G" badge Given the timer is in Late or Missed state When displayed Then it does not use the amber Grace color or the "Grace" label Given the timer transitions from Grace to Late/Missed at T + G When observed Then there is no intermediate state labeled "Grace" after T + G
Grace Notifications and Reminders
"As a busy professional, I want a timely nudge when grace begins so that I can tap in quickly without monitoring the app."
Description

Deliver optional, rate-limited nudges that respect user preferences and quiet hours: a pre-expiry reminder (e.g., 2 minutes before window end) and a grace-start notification with a live countdown and deep link to one-tap check-in. On desktop/web, surface in-app banners if push is disabled. Throttle to one nudge per window to minimize noise, and localize copy to be concise and action-oriented. Ensure notifications are consistent with the configured grace length and update immediately if room settings change before the window closes.

Acceptance Criteria
Pre-Expiry Reminder Push
- Given a user has pre-expiry reminders enabled and push permissions granted, When the room’s rolling window has exactly 2 minutes remaining, Then send a single push notification within ±10 seconds that includes the room name, “2 min left”, and a deep link to one-tap check-in. - Given the user has already checked in for the current window, When the pre-expiry trigger time is reached, Then no notification is sent. - Given the device is offline at the trigger time, When connectivity resumes before the window ends, Then do not send a late pre-expiry notification.
Grace-Start Notification With Live Countdown
- Given a user has grace-start notifications enabled and push permissions granted, When the base window ends and the configured grace period (5–15 minutes) begins, Then send a single push within ±10 seconds containing a deep link to one-tap check-in and an indicated countdown equal to the configured grace length. - Given the user opens the push during the grace period, When the app screen loads, Then display a live countdown that updates every 1 second and matches server time within ±1 second. - Given the deep link is tapped during the grace period, When the app processes the action, Then complete the one-tap check-in within 1 second and attribute it to the grace window. - Given the grace period has ended, When the deep link is tapped, Then do not record a check-in and present an expired state.
Quiet Hours and Preference Respect
- Given the current time falls within the user’s configured quiet hours, When a pre-expiry or grace-start trigger occurs, Then do not deliver any push notification and do not queue it for later delivery. - Given the user has disabled Grace Notifications and Reminders for the room, When a trigger occurs, Then do not deliver any push or in-app banner for this requirement. - Given the user re-enables notifications during an active window, When a future trigger in that same window occurs, Then honor the updated preference; otherwise do not send retroactive notifications.
Rate Limiting: One Nudge Per Window
- Given a new window starts, When either a pre-expiry or a grace-start nudge is delivered, Then suppress all subsequent nudges for that user and room until the next window starts. - Given retries or multi-device sessions, When duplicate deliveries are attempted for the same user+room+window, Then ensure at-most-once delivery via idempotent de-duplication. - Given a pre-expiry nudge was suppressed due to quiet hours or permissions, When the grace-start trigger occurs and is allowed, Then deliver only the grace-start nudge (still at most one nudge total for the window).
Desktop/Web In-App Banner Fallback
- Given push permissions are denied or not granted and the user has an active desktop/web session, When the pre-expiry trigger occurs, Then display a single in-app banner within ±10 seconds showing remaining time and a “Check in” CTA that initiates one-tap check-in. - Given push permissions are denied and the grace period begins, When grace starts, Then display an in-app banner with a live countdown that updates every 1 second and auto-dismisses upon check-in or grace expiry. - Given the user dismisses the banner, When the window remains active, Then do not re-show any banner for the same window.
Dynamic Update on Room Setting Changes
- Given the room’s grace length is changed from X to Y before the base window ends, When scheduling notifications, Then recompute the grace-start trigger to align with the updated end time and countdown length Y within 5 seconds of the change. - Given a grace countdown banner or in-app timer is visible, When the grace length changes during the grace period, Then update the displayed remaining time and enforced end time within 3 seconds. - Given the grace length is reduced such that the grace period is already over, When the app is in the foreground, Then immediately expire the countdown and block one-tap check-in via deep link.
Localized, Concise Action-Oriented Copy
- Given the app locale is set to any supported language, When a push or banner is delivered, Then render title and body from localized strings with correct interpolation for room name and time remaining; fallback to English if a key is missing. - Given content length constraints, When rendering on standard mobile and desktop viewports, Then the title is ≤ 45 characters and the body is ≤ 90 characters in English, and no truncation occurs in supported locales. - Given copy guidelines, When a nudge is delivered, Then the primary action uses an imperative verb (e.g., “Check in now”) and the message includes remaining time (e.g., “2 min left”).
Server Time Sync and Offline Handling
"As a user on an unreliable connection, I want my check-in to count if I tapped within grace so that connectivity issues don’t unfairly break my streak."
Description

Use server time as the source of truth for all eligibility decisions and implement lightweight time synchronization on app open and periodically to measure device drift. When offline, queue the user’s tap with device timestamp and reconcile on reconnect: accept if, after correcting for measured drift within an allowed tolerance, the tap falls inside the grace window; otherwise, provide a clear explanatory error. Handle time zone changes, DST, and platform differences consistently, and log reconciliation outcomes for debugging and analytics.

Acceptance Criteria
Server Time Sync on App Open and Resume
Given the app launches or returns to foreground and network is available When the session begins Then the client fetches current server epoch time and computes device_drift_ms = device_time_ms − server_time_ms And stores drift and last_sync_at locally And subsequent eligibility decisions in this session use server time as source of truth (not the device clock) And if a successful sync occurred within the last 2 minutes, skip an additional fetch
Periodic Drift Refresh During Active Session
Given the app remains in foreground When 10 minutes have elapsed since the last successful time sync Then the client performs a background time sync to refresh drift And if a check-in attempt starts and the last sync is older than 2 minutes and network is available, perform a sync before evaluating eligibility And if network is unavailable, proceed using the most recent drift value without crashing
Offline Check-in Queuing and Reconciliation Within Grace Edge
Given the device is offline and the user taps check-in within the grace edge buffer (configured 5–15 minutes) When the tap occurs Then enqueue the tap with device_timestamp_ms and local_tz_offset And on reconnect, correct the tap time by applying measured device_drift_ms and tolerance ±2 seconds And if corrected_tap_time is within window_end + grace_length, accept the check-in, update the streak, and record reconciliation outcome reason_code=inside_grace
Offline Check-in Rejection with Clear Error Outside Grace Edge
Given the device is offline and the user taps check-in outside the window + grace When the app reconnects and reconciles using measured drift and tolerance ±2 seconds Then reject the check-in with reason_code=outside_window And present an error including the user's tap time (local), window end time (local), grace length, and the delta (e.g., "missed by 1m 12s") And do not update streaks or room state
Time Zone and DST Change Handling
Given a time zone change or DST transition occurs between tap and reconciliation When eligibility is evaluated Then compute all boundaries and corrected tap time in UTC using server time And render human-readable times in the user's current local time zone with correct DST rules And ensure the accept/reject decision is unaffected by the time zone change
Cross-Platform Consistency of Eligibility Decisions
Given identical account configuration and identical tap timing relative to server When eligibility is computed on iOS and Android clients and by the backend Then all three agree on accept/reject and delta_ms_from_boundary within ±1 second And platform unit tests use the same shared fixtures to verify parity
Reconciliation Logging and Analytics Events
Given any check-in decision (real-time or offline reconciliation) When the decision is made Then emit exactly one analytics/log event with fields: request_id, anonymized_user_id, device_timestamp_ms, server_time_at_decision_ms, measured_drift_ms, drift_source (fresh|cached), window_start_ms, window_end_ms, grace_length_ms, corrected_tap_time_ms, decision (accept|reject), reason_code, delta_ms_from_boundary, platform, network_state, tz_offset_minutes, dst_active, error_code (if any) And events are queryable in analytics within 5 minutes (p95) And logging failures do not affect the eligibility decision outcome
Anti-Gaming Limits and Transparency
"As a room host, I want visibility and gentle controls on grace usage so that the feature helps genuine near-misses without encouraging chronic lateness."
Description

Introduce configurable guardrails to discourage abuse while preserving flexibility: optional per-room monthly caps on grace usage, personal counters visible to members, and optional leaderboard indicators such as grace usage ratio. Detect anomalous patterns (e.g., majority of check-ins via grace over multi-week periods) and notify hosts with suggested policy tweaks. All limits are non-retroactive, do not invalidate past streaks, and are clearly communicated in UI to maintain trust and accountability.

Acceptance Criteria
Host Configures Per-Room Monthly Grace Cap
Given a room host with admin rights opens Room Settings > Grace Edge When they view the Monthly Grace Cap control Then the default state is Off When they enable the cap and enter 10 Then the setting saves successfully and persists across sessions And members in that room see a 0/10 grace uses counter for the current calendar month And invalid inputs (empty, non-integer, <= 0) show inline validation and cannot be saved
Grace Usage Counter Visible to Members
Given a member is in a room Then they can view their personal grace usage counter for the current calendar month within the room UI When the member completes a check-in during the grace window Then the counter increments by 1 within 2 seconds and persists after app restart And if a monthly cap is set to 10, the display shows n/10 used; if no cap is set, it shows n used And counters are scoped to the room and are not visible to non-members
Grace Usage Enforcement After Cap Reached
Given a room has a monthly grace cap of 5 And a member has already used 5 grace check-ins this month in that room When the member attempts a check-in during the grace window Then the app displays Grace cap reached—this check-in will not count toward your streak And the check-in does not advance or preserve the member's streak And an on-time check-in within the normal window continues to count normally And past streaks and prior check-ins remain unchanged
Non-Retroactive Changes to Caps
Given a room initially has no cap and members have accrued valid streaks When the host enables a monthly cap of 8 mid-month Then all prior check-ins and streaks remain valid and unchanged When the host later reduces the cap from 8 to 3 while a member has already used 5 grace check-ins this month Then the member's past streak remains valid and prior grace check-ins are not invalidated And remaining grace uses become 0 for the rest of the month in that room When the host disables the cap Then enforcement stops immediately while personal usage counters continue tracking for transparency
Leaderboard Grace Indicator Toggle
Given the host enables Show grace usage on leaderboard in room settings Then the leaderboard displays a grace usage ratio for each member for the current calendar month (grace uses / total check-ins) And the leaderboard ranking order is unaffected by showing the indicator When the host disables the toggle Then the indicator is hidden for all members in that room within 1 minute And the indicator is only visible to members of the room
Anomalous Grace Usage Detection and Host Notification
Given anomaly detection defines majority usage as >= 60% of a member's last 20 check-ins completed via grace over a span of at least 14 days When a member meets or exceeds this threshold Then the room host receives an in-app notification within 1 hour with the member's name, the ratio detected, the time window analyzed, and suggested actions (e.g., set/lower monthly cap, reduce grace window) And no more than one anomaly notification per member is sent within any 14-day period And no streaks or past check-ins are altered as a result of the notification
Cross-Room Isolation of Caps and Counters
Given a user participates in Room A (cap 5) and Room B (no cap) When the user completes 4 grace check-ins in Room A and 3 in Room B within the same month Then Room A displays 4/5 used with 1 remaining and enforces the cap in Room A only And Room B displays 3 used with no cap and does not enforce any limit And leaderboard indicators and anomaly detection are evaluated per room independently
Analytics, Reporting, and Rollout Controls
"As a product manager, I want robust instrumentation and a safe rollout so that we can prove impact and iterate quickly without risking user trust."
Description

Instrument events for exposures to the grace state, check-ins saved by grace, streak saves, late rejections, and notification interactions. Provide dashboards and exportable reports to track adoption and impact on adherence and retention by cohort. Ship behind a feature flag with cohort-based rollout and a kill switch for rapid disable if issues arise. Ensure event payloads include the grace tag and configuration version while respecting privacy and data minimization practices.

Acceptance Criteria
Grace Exposure Event Tagged with Config Version
Given Grace Edge is enabled for the user’s cohort and the user enters the grace window When the session transitions into grace state Then exactly one "grace_exposure" event is emitted per session with fields: grace_tag=true, config_version, cohort_id, anonymized_user_id, room_id, timestamp_utc, grace_window_minutes And the payload contains no PII (name, email, phone, IP) And the event is queryable in the analytics store within 5 minutes of emission
Check-in Saved by Grace and Streak Save Telemetry
Given a user completes a check-in within the configured grace window When the check-in is recorded Then a "checkin_saved_by_grace" event is emitted with fields: grace_tag=true, config_version, cohort_id, anonymized_user_id, room_id, checkin_at_utc, window_end_at_utc, correlation_id And duplicate emissions for the same check-in are prevented via idempotency using correlation_id And if the streak would have broken without grace, a "streak_save" event is emitted at rollover with fields: grace_tag=true, config_version, cohort_id, anonymized_user_id, previous_streak_length, new_streak_length, correlation_id And both events are queryable within 5 minutes of emission
Late Rejection and Grace Notification Interaction Events
Given a user attempts to check in after the grace window has ended When the tap occurs Then a "late_rejection" event is emitted with fields: grace_tag=false, config_version, cohort_id, anonymized_user_id, room_id, attempt_at_utc, reason="outside_grace" And if a grace reminder notification is used, "notification_delivered", "notification_opened", and "notification_action_tap" events are captured with fields: notification_type="grace", notification_id, campaign_id, config_version, cohort_id, anonymized_user_id, timestamp_utc And all notification events are linkable via notification_id and are queryable within 5 minutes And no PII is present in any payload
Cohort Adoption and Impact Dashboard
Given an analyst opens the Grace Edge dashboard When filters for time range, platform, cohort_id, and config_version are applied Then the dashboard displays metrics by cohort: exposures, exposure_rate, checkins_saved_by_grace, streak_saves, late_rejections, notification_CTR, adherence_rate, retention_7d, retention_28d And charts and tables update within 5 seconds for datasets of 90 days or fewer And selecting a cohort drills down to daily time series and config_version breakdown And control cohorts show zero for grace-tagged metrics
Exportable Cohort Impact Report with Privacy Controls
Given an analyst requests a CSV export for a selected time range and filters When the export is generated Then a CSV is downloadable within 2 minutes with columns: date, cohort_id, config_version, exposures, checkins_saved_by_grace, streak_saves, late_rejections, notifications_delivered, notifications_opened, notifications_action_tap, adherence_rate, retention_7d, retention_28d And no PII columns are included And cohorts with fewer than 10 users in the period are aggregated into "small_cohort" or suppressed And the file name includes feature name, date range, and config_version
Feature Flag Rollout by Cohort and Percentage
Given a remote feature flag "grace_edge" is defined When targeting is set to specific cohort_ids and/or a rollout percentage within a cohort Then only targeted users receive grace behavior and emit grace-tagged events with the active config_version And non-targeted users do not enter grace state and emit no grace-tagged events And changes to targeting propagate to clients on next config refresh within 60 seconds without requiring app restart And an audit log records feature flag changes with actor, timestamp, and previous/new values
Global Kill Switch Disablement
Given an operational incident requires disabling Grace Edge When the global kill switch is toggled off Then the grace state is disabled across clients on next config refresh within 60 seconds And no new grace-tagged events are emitted after disablement And dashboards reflect zero new exposures after the disable time And re-enabling restores behavior and event tagging without an app update

Window Timeline

A live progress bar that visualizes your current rolling window start, end, and next reset, including safe zones and rescue moments. Gives you instant clarity on how much time is left so you can plan check-ins with confidence.

Requirements

Rolling Window Engine
"As a habit-driven user, I want the app to precisely calculate my current window and next reset so that I can schedule my check-ins confidently and avoid losing my streak."
Description

A deterministic service that calculates each habit’s rolling window start, end, and next reset, plus safe-zone and rescue-moment thresholds based on cadence (daily, hourly, custom), user timezone/locale, daylight saving transitions, and grace rules. It exposes a consistent contract to clients and rooms, updates in real time on clock/timezone changes, and aligns with streak validation logic to prevent false streak decay. Designed to support individual habits and micro-commitment rooms, ensuring parity across devices and server for trust and predictability.

Acceptance Criteria
Deterministic API contract for rolling window output
Given a habit with cadence=daily, timezone=America/Los_Angeles, safeZonePercent=60, rescueMinutes=0, and referenceTime=2025-01-15T10:30:00-08:00 When the Rolling Window Engine computes the window Then the response includes fields: windowStart, windowEnd, nextReset, safeZoneStart, safeZoneEnd, rescueStart, cadence, timezone, generatedAt And windowStart=2025-01-15T00:00:00-08:00 and windowEnd=2025-01-16T00:00:00-08:00 And nextReset equals windowEnd And safeZoneStart equals windowStart and safeZoneEnd=2025-01-15T14:24:00-08:00 (60% of the window) And rescueStart equals windowEnd (0-minute rescue) And all timestamps are ISO-8601 with timezone offsets and timezone is a valid IANA ID And calling compute twice with identical inputs returns identical values for all fields except generatedAt
Daily cadence across DST forward (America/Los_Angeles 2025-03-09)
Given a habit with cadence=daily, timezone=America/Los_Angeles, safeZonePercent=50, rescueMinutes=30, and referenceTime=2025-03-09T12:00:00-07:00 When the Rolling Window Engine computes the window Then windowStart=2025-03-09T00:00:00-08:00 and windowEnd=2025-03-10T00:00:00-07:00 And the window duration equals 23 hours And nextReset equals windowEnd And safeZoneStart equals windowStart and safeZoneEnd=2025-03-09T11:30:00-08:00 (50% of the 23-hour window) And rescueStart=2025-03-09T23:30:00-07:00 (30 minutes before end) And a check-in at 2025-03-09T23:59:45-07:00 is attributed to this window, preventing streak decay
Daily cadence across DST backward (America/Los_Angeles 2025-11-02)
Given a habit with cadence=daily, timezone=America/Los_Angeles, safeZonePercent=50, rescueMinutes=30, and referenceTime=2025-11-02T12:00:00-08:00 When the Rolling Window Engine computes the window Then windowStart=2025-11-02T00:00:00-07:00 and windowEnd=2025-11-03T00:00:00-08:00 And the window duration equals 25 hours And nextReset equals windowEnd And safeZoneStart equals windowStart and safeZoneEnd=2025-11-02T12:30:00-08:00 (50% of the 25-hour window) And rescueStart=2025-11-02T23:30:00-08:00 (30 minutes before end) And a check-in at 2025-11-02T23:59:45-08:00 is attributed to this window, preventing streak decay
Hourly cadence with safe-zone and rescue thresholds
Given a habit with cadence=hourly, timezone=America/New_York, safeZonePercent=60, rescueMinutes=5, and referenceTime=2025-10-01T14:26:00-04:00 When the Rolling Window Engine computes the window Then windowStart=2025-10-01T14:00:00-04:00 and windowEnd=2025-10-01T15:00:00-04:00 And nextReset equals windowEnd And safeZoneStart equals windowStart and safeZoneEnd=2025-10-01T14:36:00-04:00 (60% of the hour) And rescueStart=2025-10-01T14:55:00-04:00 (5 minutes before end) And a check-in at 2025-10-01T14:57:30-04:00 is attributed to this window and marked as within rescue moment And a check-in at exactly 2025-10-01T15:00:00-04:00 is attributed to the next window
Real-time recompute on timezone change and manual clock shift
Given a habit with cadence=hourly and the app streaming live window updates And the user’s device timezone is America/New_York at 2025-10-01T15:00:10-04:00 When the device timezone is changed to America/Los_Angeles Then within 1 second the engine emits an updated window where windowStart=2025-10-01T12:00:00-07:00 and windowEnd=2025-10-01T13:00:00-07:00 And the emitted payload reflects timezone=America/Los_Angeles When the system clock is manually adjusted forward by 5 minutes during an active window Then within 1 second the engine recomputes and emits the adjusted window boundaries consistent with the new local time, without emitting overlapping or out-of-order windows
Cross-device/server parity and streak validation alignment
Given the same habit configuration (cadence, timezone, safeZonePercent, rescueMinutes) and referenceTime When computed on iOS, Android, Web client, and Server Then windowStart, windowEnd, nextReset, safeZoneStart, safeZoneEnd, rescueStart, cadence, timezone are identical across platforms (excluding generatedAt) And the interval is half-open: check-ins where windowStart <= t < windowEnd are attributed to the current window; t == windowEnd is attributed to the next window And if at least one check-in exists in the current window, Then streak does not decay at nextReset; otherwise it decays exactly at nextReset And for room-scoped habits with roomTimezone=UTC, all members (regardless of device timezone) receive windows aligned to UTC boundaries and identical outputs
Custom cadence: 36-hour rolling window with thresholds
Given a habit with cadence=custom(durationHours=36), timezone=UTC, safeZonePercent=50, rescueMinutes=10, and habitStartAt=2025-09-30T08:00:00Z And referenceTime=2025-10-01T14:30:00Z When the Rolling Window Engine computes the window Then windowStart=2025-09-30T08:00:00Z and windowEnd=2025-10-01T20:00:00Z And nextReset equals windowEnd And safeZoneStart equals windowStart and safeZoneEnd=2025-10-01T02:00:00Z (18 hours after start) And rescueStart=2025-10-01T19:50:00Z (10 minutes before end) And a check-in at 2025-10-01T14:30:00Z is attributed to this window; a check-in at 2025-10-01T20:00:00Z is attributed to the next window
Live Window Timeline Bar
"As a user in a live room, I want a clear visual timeline showing how much time is left and when I’m safe or at risk so that I can decide when to check in."
Description

An interactive, battery-efficient progress bar that visualizes elapsed and remaining time within the rolling window, labeling start, end, and next reset, and shading safe zones and rescue moments. It animates smoothly, updates continuously without jank, and supports tap for details. The component is responsive (mobile/desktop), themeable, and accessible (screen reader labels, high-contrast, reduced motion). It integrates with live rooms and one-tap check-ins to reflect status instantly and maintain visual consistency with StreakShare’s brand.

Acceptance Criteria
Accurate Rolling Window Progress and Labels
- Given a configured rolling window [start, end], When the current time advances, Then the progress value equals (now − start)/(end − start) within ±0.5% and updates at least once per second. - Given the moment the end time is reached, When the window resets, Then the progress resets to 0 within 200ms and the start/end/next reset reflect the new window. - Given the user’s locale and 12/24h preference, When rendering labels, Then Start, End, and Next reset times are shown in local time and update no later than the next whole minute. - Given a timezone or DST change event, When the system time zone shifts, Then labels and progress recalculate within 1 second without showing overlapping or backwards progress.
Safe Zone and Rescue Moments Visualization
- Given safe zone and rescue thresholds (e.g., first X% safe, last Y% rescue), When the bar renders, Then shaded segments map exactly to those durations with segment boundaries at the correct timestamps. - Given theme color tokens, When displaying segments, Then safe and rescue segments use their designated tokens and remain visually distinct from the base progress. - Given configuration changes to thresholds, When updated, Then segment shading updates within 300ms without a full component remount or flicker.
Smooth, Jank-Free, Battery-Efficient Animation
- Given a device with 60Hz display, When the timeline is animating in the foreground, Then average frame rate is ≥55 FPS with <5% dropped frames over 60 seconds. - Given performance profiling, When the timeline runs idle (no user interaction), Then main-thread long tasks >50ms do not occur more than once per minute. - Given Low Power Mode or battery saver is active, When detected, Then the component reduces update frequency to ≤2 Hz and pauses animation when offscreen or app is backgrounded. - Given the component is not visible (off-viewport), When scrolled out or minimized, Then animation ticks are suspended within 200ms.
Tap for Details Interaction
- Given the user taps the timeline bar, When the gesture is recognized, Then a details panel opens within 200ms. - Given the details panel is open, When rendered, Then it displays start time, end time, next reset, percent elapsed, and time remaining with second-level precision. - Given the panel is open, When the user taps outside, presses Esc, or swipes down (mobile), Then the panel dismisses and focus returns to the originating control. - Given keyboard users, When the bar is focused and Enter or Space is pressed, Then the same details panel opens.
Responsive and Themeable Component
- Given screen width ≤768px, When rendering, Then bar height is 8–10px, labels collapse to compact format, and no horizontal scroll is introduced. - Given screen width ≥1024px, When rendering or resizing, Then the bar expands to container width, label text is fully visible, and no layout shift greater than 1px occurs during animation. - Given a theme change (light/dark/brand variant), When toggled, Then colors and typography swap to the active token set within 200ms without flashing unstyled content. - Given device orientation changes, When rotated, Then the bar and labels reflow without clipping or overlap.
Accessibility and Reduced Motion Support
- Given assistive technology users, When the bar is focused, Then it exposes role=progressbar with aria-valuemin=0, aria-valuemax=100, and aria-valuenow reflecting current percent to the nearest whole number. - Given screen readers, When the bar updates, Then an aria-live="polite" label announces “X percent complete, Y remaining, next reset at Z” no more than once every 5 seconds. - Given prefers-reduced-motion is enabled, When detected, Then continuous animation is disabled and the progress updates in discrete 1-second steps with fade transitions ≤100ms. - Given high-contrast mode, When enabled, Then contrast ratio is ≥4.5:1 between bar/track and ≥3:1 at segment boundaries. - Given keyboard navigation, When tabbing, Then the bar is focusable with a visible focus indicator and Enter/Space triggers details.
Live Room and One-Tap Check-In Sync
- Given a user performs a one-tap check-in in a live room, When the event is acknowledged by the server, Then the timeline reflects the check-in state and any check-in marker within 300ms. - Given live room window settings change (e.g., window length, safe/rescue thresholds), When updated by host, Then all participants’ timelines update within 1 second via real-time sync. - Given temporary network loss, When disconnect occurs, Then the timeline shows a stale-state indicator within 2 seconds and reconciles to the latest state within 2 seconds of reconnection. - Given brand consistency requirements, When rendered inside live rooms and profile views, Then the timeline uses the same tokens, spacing, and corner radii to within 1px of the design spec.
Smart Countdown & Alerts
"As a busy remote worker, I want timely reminders before my window resets so that I don’t miss my check-in."
Description

A real-time countdown layer that surfaces remaining time to safe-zone end and window reset, with opt-in reminders at configurable thresholds (e.g., 30 minutes, 5 minutes, rescue moment). It supports in-app banners and OS-level notifications, respects user preferences and Do Not Disturb, deduplicates across devices, and degrades gracefully under background execution limits. Alerts are context-aware for rooms and individuals, nudging timely check-ins to prevent streak decay without creating notification fatigue.

Acceptance Criteria
Accurate real-time countdown and rescue highlight
Given a room window with start 10:00, safe‑zone end 21:00, and reset 22:00 and server time is 20:30:00 with the app in foreground on a stable network When the timeline renders Then the safe‑zone remaining displays 00:30:00 ±1s and window reset remaining displays 01:30:00 ±1s Given the timeline is visible When 10 seconds elapse Then each remaining time decrements by 00:00:10 ±1s and tick frequency is at least 1 Hz while in foreground Given rescue moment is configured to 00:02:00 before reset and current server time reaches 21:58:00 When the timeline updates Then rescue state is highlighted and a rescue banner is shown within 2 seconds Given the client clock is skewed by +3 minutes relative to server When time sync occurs Then displayed countdown corrects to server time within 2 seconds and remains within ±1 second thereafter
Configurable threshold reminders delivery
Given reminders are enabled for 30m, 5m, and rescue thresholds and push permission is granted and the app is in background When remaining time to safe‑zone end crosses the 30m boundary Then exactly one OS‑level notification is delivered within 10 seconds and logged in the in‑app notification history Given the app is in foreground at the 30m boundary When the threshold is reached Then an in‑app banner appears within 2 seconds and no OS‑level notification is shown Given a user performs a successful check‑in before a scheduled threshold fires When the check‑in is recorded Then all pending reminders for that window are canceled and no further alerts for that window are sent Given a user leaves or mutes a room When thresholds are reached for that room Then all scheduled reminders for that room are canceled within 5 seconds and no alerts are issued
Respect Do Not Disturb and user preferences
Given OS Do Not Disturb is enabled and the user setting "Respect DND" is on When a threshold triggers Then notifications are delivered silently (no sound or vibration) or suppressed per OS policy and only in‑app banners appear if the app is foreground Given OS Do Not Disturb is enabled and the user setting "Respect DND" is off When a threshold triggers Then notifications are delivered with sound and vibration per user notification settings Given a user disables reminders for a specific room When thresholds occur for that room Then neither OS‑level notifications nor in‑app banners are generated Given the user has configured quiet hours from 22:00 to 07:00 When thresholds occur during quiet hours Then alerts are delayed until quiet hours end or until the window resets, whichever occurs first
Cross‑device alert deduplication
Given a user is signed in on iPhone and iPad with notifications enabled on both When a 5m threshold triggers Then at most one OS‑level notification is delivered across devices within 10 seconds Given one device has the app in foreground and another is background When a threshold triggers Then only the foreground device shows the in‑app banner and no OS‑level notification is sent to either device Given an alert for a threshold is delivered on one device and dismissed When the same threshold would fire again due to retries or reconnects Then other devices do not deliver duplicate notifications for that threshold within the same window Given network retries or race conditions occur When deduplication is applied Then no more than one alert per threshold per room is delivered within a 10‑minute dedupe window
Graceful degradation under background limits
Given the app is force‑quit on iOS and server‑side push delivery is available When a configured threshold triggers Then the user receives a remote push notification within the platform SLA for delivery Given both background refresh and push delivery fail When the user next opens the app during the same window Then a catch‑up banner is shown within 2 seconds indicating the missed threshold if the window has not yet reset Given the OS terminates background tasks while multiple thresholds are scheduled When background execution limits are hit Then at least the next upcoming threshold is delivered via a scheduled local notification and lower‑priority thresholds are skipped to prevent spam Given battery saver mode is enabled When thresholds trigger Then only the highest‑priority alert (rescue) is delivered and earlier reminders are suppressed
Context‑aware content and suppression
Given the user has not checked in for the current window When a threshold triggers Then the alert text includes the room name, remaining time, and the user’s current streak count and the CTA deep‑links to the room check‑in action Given the user has already checked in for the current window When subsequent thresholds occur for that room Then no further alerts are sent until the next window begins Given two rooms reach thresholds within 1 minute of each other When alerts are generated Then a single grouped notification is delivered listing each room and its remaining time Given a room enforces a specific check‑in type (e.g., photo) When an alert fires Then the CTA opens directly to the required check‑in flow for that room
Notification fatigue controls: rate limit and snooze
Given the default rate limit is 3 alerts per room per window When additional thresholds would exceed this limit Then excess alerts are suppressed for that room and window Given the user taps "Snooze" for 60 minutes on an alert When subsequent thresholds occur during the snooze period Then all alerts for that room are paused until the snooze ends or the window resets, whichever occurs first Given the user changes reminder thresholds to only "rescue" When the window progresses Then only the rescue alert is scheduled and all other threshold schedules are canceled Given the user is in 3 rooms with thresholds within a 15‑minute span When alerts are evaluated Then cross‑room rate limiting caps total alerts to a maximum of 4 in any rolling 15‑minute period
Timezone Shift Resilience
"As a traveling creator, I want the timeline to adapt to new timezones without penalizing my streak so that I can keep habits while on the move."
Description

Automatic detection and handling of timezone and DST changes, recalculating rolling windows while preserving streak integrity. The timeline explains shifts to the user, shows local and original times when relevant, and applies configurable grace where allowed. Server and clients maintain a history of window definitions for auditability and conflict resolution. This ensures travelers and distributed teams experience consistent timelines and fair streak outcomes.

Acceptance Criteria
OS Timezone Change: Online Recalculation and Explanation Banner
Given the client is online and the OS timezone changes (e.g., UTC−5 to UTC−7) When the app detects the timezone change event Then the current rolling window boundaries are recalculated using the new local offset within 500 ms And the user’s active streak remains intact if the prior window completion criteria were met And the Window Timeline updates safe and rescue zones to the new boundaries within 1 s And an explanation banner appears within 1 s showing old offset, new offset, and effective time And the timeline displays both original and local times for the affected window segment And a new window definition version is persisted locally and acknowledged by the server within 2 s
DST Forward Jump: Preserve Streak and Grace Application
Given the user’s region enters DST causing a +1 hour skip at 02:00 local time When the transition occurs during an incomplete rolling window Then the window does not become impossibly short relative to policy minimum duration And configurable grace (up to policy max, e.g., 60 minutes) is applied once to preserve fairness And the Window Timeline shows a rescue moment with a countdown and revised reset time And one check-in within the grace window is accepted and counted once toward the streak And the timeline annotates the DST jump and the recalculated boundaries And server and client window definition histories are consistent after sync
DST Backward Repeat Hour: No Double Counting and Disambiguation
Given the user’s region exits DST causing a −1 hour repeat at 02:00 local time When two check-ins occur with identical local clock labels across the repeated hour Then both check-ins are stored with unique UTC timestamps and offsets And only one window’s credit is applied per rolling window per policy (no double advance due to repeated local time) And window boundaries are recorded with absolute UTC and offset disambiguators And the Window Timeline labels the overlapping segment and safe zone accurately And streak continuity remains correct after the transition
Multi-Timezone Travel With Offline and Server Reconciliation
Given the user crosses 2 or more timezones while offline for up to 24 hours When the client reconnects to the server Then the client uploads detected offset changes and local window events with UTC stamps And the server reconciles a canonical ordered window history using UTC and policy And conflicts are resolved deterministically (server-wins using version vectors) And all active devices render a matching Window Timeline within 3 s of reconciliation And no streak is broken if at least one valid check-in exists per UTC-defined window And an explanation card lists each shift with local and original times for the affected windows
Window Definition History and Export
Given a user requests window history for a specific date range When the request is made via API or in-app viewer Then the system returns entries with fields: window_id, utc_start, utc_end, local_start, local_end, tz, offset, dst_flag, reason, client_id, version, created_at And history includes all revisions caused by timezone/DST shifts and reconciliations And results are returned within 2 s for ranges within the last 30 days And CSV and JSON exports are available and match the API record count exactly And each entry links to associated check-ins and any displayed shift banners
Configurable Grace Windows Applied Fairly
Given an organization configures grace_max_minutes=60 and allowed_contexts=[DST, timezone_shift, travel] When a qualifying shift occurs and the user has not yet checked in for the current window Then grace is applied once per affected window up to the configured maximum And the Window Timeline shows a grace badge with a countdown and exact expiry timestamp And check-ins after grace expiry do not preserve the streak and display a reason message And grace is not applied outside allowed contexts or beyond policy limits And all grace applications are logged in window history with the policy snapshot
Unrealistic Clock Jumps: Server Trust and User Confirmation
Given the client clock jumps by more than 3 hours without an OS timezone change or plausible geolocation change When the app detects a discrepancy versus server time Then the client requests server-derived window definitions and ignores local clock for calculation And a warning is shown requiring user confirmation, explaining the override and impact And streak integrity is determined by server UTC windows to ensure fairness And the event is logged with reason=clock_anomaly in window history And the Window Timeline displays original versus overridden assumptions for the affected window
Check-in Synchronicity
"As a participant, I want the timeline to update instantly when I check in so that I see my streak is safe and others see my progress."
Description

Tight coupling between the timeline and one-tap check-ins: upon check-in, the timeline locks in the streak, updates safe/rescue indicators immediately, and disables duplicate actions until the next eligible window. In micro-commitment rooms, state changes broadcast in real time for reactions. Supports undo within defined grace, with recalculated boundaries and authoritative server reconciliation to keep everyone in sync.

Acceptance Criteria
Immediate Timeline Lock on Check-in
Given the user is inside an eligible check-in window and the timeline is visible When the user performs a one-tap check-in Then the current window is locked locally within 150 ms, the streak count increments by 1, the progress bar reaches 100%, and the next reset timestamp is displayed And Then safe/rescue indicators switch to "Safe" state and show the remaining time until reset And Then the check-in control displays a "Locked" state until the next eligible window begins
Disable Duplicate Check-ins Until Next Window
Given a successful check-in has been recorded for the current window When the user attempts to tap the check-in button again before the next eligible window Then the button is disabled, no additional check-in event is sent, and no streak change occurs And Then the UI indicates "Already checked in" for the current window When the next eligible window starts Then the check-in button re-enables within 1 second
Real-time Room Broadcast of Check-in State
Given a micro-commitment room with at least two connected members When member A checks in Then all other members receive the check-in event and see member A’s timeline lock, streak increment, and reactions enabled within 2 seconds And Then exactly one event is delivered per user per window (no duplicates) When any member posts a reaction to the check-in Then the reaction appears for all room members within 2 seconds and is associated with the correct locked window
Undo Within Grace Period Recalculates Timeline
Given undo_grace_seconds = 60 and a locked check-in for the current window When the user taps Undo within 60 seconds of the check-in Then the streak count decrements by 1, the timeline unlocks for the current window, and safe/rescue indicators recalculate based on current time And Then the check-in button re-enables for the current window if still eligible; otherwise remains disabled until the next window When undo_grace_seconds has elapsed Then the Undo option is hidden and cannot be triggered via UI or API And Then an undo event is broadcast to room members within 2 seconds
Server-Authoritative Reconciliation and Idempotency
Given clients may be offline or on multiple devices When multiple check-in requests for the same user and window are received Then the server persists exactly one check-in per window, ignores duplicates via an idempotency key, and returns the authoritative state And Then all connected clients reconcile to the server-authoritative state within 3 seconds, updating timeline lock, streak, and indicators accordingly When a previously optimistic local lock conflicts with server state Then the client displays a brief sync correction and emits a correction event to the room
Safe Zone and Rescue Moment Threshold Accuracy
Given server configuration safe_threshold_percent, rescue_threshold_percent, and rescue_min_minutes When the time remaining in the current window crosses these thresholds Then the timeline shows Safe when time_remaining >= window_duration * safe_threshold_percent And shows Rescue when time_remaining <= max(window_duration * rescue_threshold_percent, rescue_min_minutes) And Then threshold changes are reflected in UI within 1 second and are consistent across all clients in the same room When the window resets Then the progress bar resets to 0%, indicators clear, and the next window start/end times are shown within 1 second
Offline-First Consistency
"As a user with spotty internet, I want the timeline to work offline and keep my streak accurate so that I can check in without worry."
Description

Local prediction of window boundaries and safe/rescue status when offline, queuing check-ins and reconciling with the server on reconnect. Implements conflict resolution and drift correction, warns on clock skew, and ensures eventual consistency across devices. The timeline clearly indicates offline state and sync progress so users can act confidently without risking streak loss.

Acceptance Criteria
Offline Timeline Prediction Accuracy
Given the device enters offline mode with a last successful sync timestamp and server time offset When the timeline renders the current rolling window start/end and safe/rescue status locally Then the predicted window boundaries match server-resolved boundaries after next sync within max(2% of window length, 10 seconds) And the predicted safe/rescue status for any check-in opportunity during that window matches the server-resolved status post-sync And the countdown updates at least once per second while the app is active
Queued Offline Check-ins and Idempotent Sync
Given the user performs one or more check-ins while offline When connectivity is restored Then all queued check-ins are submitted in chronological order within 5 seconds of reconnect And duplicate submissions are prevented via idempotency keys And each check-in receives a server outcome (accepted or ignored) and the timeline updates within 1 second of each response And the final streak count equals the server-authoritative value after sync completes
Conflict Resolution Across Devices
Given a conflicting check-in exists from another device for the same rolling window When the offline device syncs its queued check-ins Then exactly one check-in is counted for that window using server-authoritative ordering (earliest valid timestamp wins) And superseded local check-ins are marked ignored with reason "conflict—duplicate window" And all signed-in devices display identical streak count and window boundaries within 3 seconds of sync completion
Clock Skew Detection and Drift Correction
Given the detected local-to-server clock offset exceeds 30 seconds When the app detects this on reconnect or via heartbeat Then a visible "Clock out of sync" warning appears within 1 second And the timeline immediately applies the server-provided offset for predictions And after correction, the countdown and window boundaries align to server within 1 second
Offline State and Sync Progress UI
Given the device loses connectivity When the next timeline refresh occurs Then an "Offline" indicator appears within 500 milliseconds and persists until sync completes And the UI displays the number of queued actions and a progress state during sync And upon completion, a "Synced" confirmation is shown for at least 2 seconds and the offline indicator is removed
Rescue Zone Safety Buffer
Given the app is offline and shows a rescue zone ending at predicted time T_pred And the user checks in at or before T_pred When the device reconnects and reconciles with the server Then the check-in counts for the window even if the server window end differs by up to 30 seconds And the offline rescue end includes a safety buffer of at least 30 seconds earlier than the last known server window end And no streak loss occurs for check-ins made within displayed safe or rescue zones
Eventual Consistency Across Devices
Given multiple devices are signed in to the same account When any device completes a sync after offline activity Then within 5 seconds all devices display the same streak count, current window start/end, and safe/rescue status And no device displays an offline indicator once all queued actions are reconciled
Performance & Telemetry Guardrails
"As a product owner, I want visibility into how the timeline performs and impacts behavior so that we can iterate and scale confidently."
Description

Instrumentation and guardrails for the timeline: measure compute and render latency, battery/CPU impact, and error rates in window calculations. Provide feature flags for staged rollout and A/B testing of safe/rescue messaging. Define performance budgets and dashboards with alerts to catch regressions early, enabling data-driven iteration that improves adherence without degrading app performance.

Acceptance Criteria
Window Timeline Render & Compute Latency Budget
Given the Window Timeline is visible on a reference mid-tier device, when the timeline updates once per second, then p95 window calculation time per tick <= 2ms and p99 <= 5ms. Given the Window Timeline is animating progress, when frames are rendered over a 5-minute session, then p95 main-thread frame render time <= 16ms and dropped frames <= 1% during idle and <= 5% during interactions. Given the Timeline screen first appears, when initial content is rendered, then time-to-first-meaningful-render <= 300ms after data is available. Given the Window Timeline runs for 5 minutes, when memory allocations are sampled, then transient allocations attributable to the timeline <= 2MB total and no GC pauses > 50ms p95.
Battery and CPU Impact Guardrails
Given a 5-minute active viewing session, when CPU is sampled at 1Hz, then p95 CPU utilization attributable to the timeline <= 15% and median <= 8%. Given the same session, when battery drain is estimated via platform APIs, then projected drain <= 6% per hour median and <= 8% per hour p95. Given the app is backgrounded with the Window Timeline mounted, when observed for 10 minutes, then timeline updates are suspended, CPU p95 <= 1%, and no scheduled timers or wake locks persist. Given poor thermal conditions, when OS thermal warnings are emitted, then the timeline reduces update frequency within 2 seconds and stabilizes CPU p95 <= 10%.
Telemetry Coverage, Quality, and Privacy for Timeline
Given any timeline session, when telemetry is collected, then events include compute_latency_ms, render_frame_time_ms, dropped_frames_pct, cpu_pct_avg, battery_drain_est_pct_per_hr, window_calc_error_count, feature_flag_state, and schema_version. Given events are validated client-side, when sent, then 99.9%+ conform to the schema and are accepted server-side. Given production sampling, when remote configuration is at default, then timeline telemetry sampling rate is 20% (100% in dogfood/staging) and is remotely adjustable without app release. Given a user has opted out of analytics or is in a consent-required state without consent, when a timeline session occurs, then no timeline telemetry is persisted or transmitted. Given a timeline session ends online, when events are queued, then 95%+ are delivered within 120 seconds; offline sessions are queued and retried for up to 72 hours with deduplication.
Feature Flags and Kill Switch for Timeline and Messaging
Given remote config is updated, when the window_timeline_enabled flag toggles, then the timeline compute/render activates or deactivates within 60 seconds of fetch without app restart. Given the kill switch is ON, when the Timeline screen loads, then the component does not compute or render the progress bar and a fallback time-left text is shown with no crashes. Given messaging_variant flag values (control|safe|rescue), when assigned to a user, then the assignment is sticky for 30 days, logged on exposure, and overridable via QA device override. Given a staged rollout, when percentage is adjusted from 1% to 50%, then the effective population shifts within 5 minutes and is observable in the rollout dashboard.
A/B Test Enablement for Safe/Rescue Messaging
Given an experiment definition, when users become eligible, then randomization assigns users to control/variant according to configured split (default 50/50) with namespace isolation across concurrent experiments. Given a user was assigned, when they re-open the app within 30 days, then the same variant is served unless experiment id changes. Given exposure logging, when the messaging is shown, then a single exposure event is recorded per session with user_id, variant, app_version, and flag states populated. Given the data pipeline, when a day closes, then variant-level metrics (adherence rate, average check-in time vs. window, p95 compute/render, battery drain) are available on the dashboard by 09:00 UTC next day.
Error Rate SLAs and Alerts for Window Calculations
Given production traffic of at least 1,000 active users in a 15-minute window, when window_calc_error events exceed 0.1% of sessions or missing window boundaries exceed 0.05%, then a Sev-2 alert is sent to Slack/PagerDuty within 5 minutes with app version, OS, device model, flag and variant context. Given normal operation, when observed over 24 hours, then window_calc_error rate <= 0.1% per user-day and timeline component crash-free sessions >= 99.9%. Given an alert fires, when the runbook link is followed, then the dashboard deep link loads within 5 seconds and shows correlated spikes by app version and rollout cohort.
Observability Dashboards for Timeline Performance
Given an engineer opens the timeline dashboard, when data loads, then charts display p50/p95 compute latency, p50/p95 render frame time, dropped frames %, CPU %, battery drain %/hr, error rates, flag adoption, and experiment splits with filters for OS, device class, and app version. Given real-time monitoring, when a metric changes, then near-real-time panels update with <= 5-minute data latency; daily rollups complete by 09:00 UTC with no gaps. Given team access, when a team member with standard permissions visits, then the dashboard loads without additional access requests and has 99.5% monthly availability. Given a new release, when the release is deployed, then a release annotation automatically appears on the dashboard within 10 minutes.

Room Flex Sync

In shared rooms, applies rolling windows per user while presenting a unified activity band for the group. Everyone can check in fairly within their own 20–28-hour window, and hosts see who’s in-window now to time prompts and reactions.

Requirements

Per-User Rolling Window Engine
"As a room member, I want my check-in window to roll based on my last check-in within a 20–28-hour range so that I can maintain a fair streak despite variable daily schedules."
Description

Implements a per-user, per-room rolling check-in window configurable between 20 and 28 hours, anchored to the user’s last qualified check-in. Computes next window start/end in real time, persists window state, and exposes APIs for clients to query current eligibility. Handles misses, window resets, and anchor updates deterministically to ensure fairness and prevent streak decay due to schedule variability. Integrates with the core streak engine, notifications, and analytics to keep streaks accurate while supporting one-tap check-ins across devices.

Acceptance Criteria
Anchor Update and Next Window Computation
- Given a user U in room R with window length L hours and a qualified check-in at T0 (server time UTC), When the check-in is accepted, Then the current window is [T0, T0 + L) and U is in-window until T0 + L exclusive. - Given U performs the next qualified check-in at T1 where T0 <= T1 < T0 + L, When the check-in is accepted, Then the anchor updates to T1, the new window is [T1, T1 + L), and no further check-ins by U in R are accepted until T1 + L. - Given U attempts a second check-in in the same window [T1, T1 + L), When the request is processed, Then the service returns 409 Conflict with code CHECKIN_ALREADY_RECORDED and does not change the anchor.
Configurable Window Length Enforcement (20–28h)
- Given room R has per-user window length L in hours, When L is set to any value outside [20,28], Then validation fails with 400 Bad Request and code WINDOW_LENGTH_OUT_OF_RANGE. - Given L is updated from L1 to L2 within [20,28], When a user U has an open window [A, A + L1), Then that window continues to use L1, and the first window created after U’s next qualified check-in uses L2. - Given a user joins room R after L was set, When their first qualified check-in is accepted, Then their window uses the current L. - Given the eligibility API is queried, Then it returns the effective L for U in R.
Missed Window Reset and Streak Impact
- Given U has window [A, A + L) and no qualified check-in occurs before A + L, When the window end is reached, Then the next window becomes [A + L, A + 2L) and U is out-of-window until A + L. - Given a miss occurs as above, When the streak engine is updated, Then U’s streak for R is decremented or reset per core streak rules, and an analytics event window_missed is emitted with anchor=A, window_length=L, room_id=R. - Given U checks in at T2 where T2 >= A + L and T2 < A + 2L, When the check-in is accepted, Then the anchor becomes T2 and the new window is [T2, T2 + L).
Eligibility API Deterministic Status and Timing
- Given the client calls GET /rooms/{R}/users/{U}/window at server time Ts, When the response is returned, Then it includes fields: inWindow (boolean), windowStart, windowEnd (ISO 8601 UTC), secondsRemaining (non-negative integer), and effectiveWindowLengthHours. - Given two identical eligibility queries within 1 second, When processed under the same server clock tick, Then the responses are identical. - Given the current time equals windowEnd exactly, When the eligibility is computed, Then inWindow is false (end is exclusive). - Given a host calls GET /rooms/{R}/window/in-window-users, When processed at Ts, Then the service returns the set of U for whom Ts ∈ [windowStart, windowEnd) with a response time P95 ≤ 200 ms for up to 1,000 users. - Given an invalid room or user ID, Then the API returns 404 Not Found.
Concurrency and Idempotency Across Devices
- Given two devices D1 and D2 submit a qualified check-in for U in R within a 200 ms window with the same idempotency key K, When processed, Then exactly one check-in is recorded, the anchor updates once, and both requests return success 200/201 with the same check-in ID. - Given D1 and D2 submit without the same idempotency key and both within the same window, When processed, Then one request succeeds and the other returns 409 Conflict CHECKIN_ALREADY_RECORDED, and the final window reflects the timestamp of the accepted check-in. - Given network retries cause duplicate submissions with K within 1 hour, Then duplicates are deduplicated and no additional side effects (streak increment, analytics) are emitted.
Persistence and Recovery of Window State
- Given the service restarts, When eligibility is queried for U in R, Then windowStart and windowEnd are derived from the last qualified check-in event and configured L with no loss of accuracy. - Given a client device’s clock is skewed by ±10 minutes, When it attempts a check-in, Then eligibility is decided by server time only, and the outcome matches what would occur with a correct client clock. - Given the database is temporarily unavailable for up to 30 seconds during a user’s in-window period, When it recovers, Then no accepted check-ins are lost, and eligibility API resumes returning correct values within 5 seconds.
Integration: Streak Engine and Analytics Events
- Given a qualified check-in is accepted, When integration is executed, Then the streak engine increments or maintains U’s streak as appropriate for R and emits a streak_updated event within 2 seconds. - Given a qualified check-in is accepted, Then an analytics event checkin_recorded is emitted with fields {user_id, room_id, checkin_time, window_start, window_end, window_length_hours, in_window:true} exactly once. - Given a check-in attempt is rejected due to out-of-window or duplicate, Then an analytics event checkin_rejected is emitted with a reason code and no changes to streak. - Given L is changed for R, Then analytics event window_length_changed is emitted with previous and new values, and the change does not retroactively modify historical windows.
Unified Activity Band Visualization
"As a member, I want to see a unified activity band that reflects the group’s current and near-future availability so that I know the best times to check in and engage."
Description

Renders a room-level activity band that aggregates individual member windows into a single, continuously updating timeline. Visually indicates who is currently in-window, who is ending soon, and forecasted near-term availability peaks without revealing exact personal schedules. Supports interactive hover/tap to reveal member counts, avatars, and engagement entry points (check-in, react). Optimized for mobile and desktop, accessible, and resilient to large rooms through progressive loading and batching. Integrates with host tools and member views to guide timing of prompts and reactions.

Acceptance Criteria
Real-Time Aggregation and Update Latency
Given a shared room with members across time zones When a member enters, exits, or reaches <=15 minutes remaining in their window Then the unified activity band reflects the change for all viewers within 3 seconds And the in-window and ending-soon counts are consistent across clients within 5 seconds And no screen flashes or duplicate events occur during the update cycle
Privacy-Preserving Indicators (No Exact Schedules)
Given any viewer hovers or taps the activity band When individual indicators are shown (in-window, ending soon) Then no exact timestamps or countdown minutes are displayed for any member And individual tooltips show status labels only ("In Window", "Ending Soon") and avatar/name, without start/end times And group-level forecasts show ranges (e.g., "peak in 30–60 min") without exposing any single member's timing
Forecasted Availability Peaks Visualization
Given the current time within a room session When the band renders forecasted availability for the next 120 minutes Then a forecast overlay displays predicted aggregated availability with peak markers and ranges And the forecast values and peak positions match the server forecast API within one render cycle And the forecast refreshes at least every 5 minutes or on window state changes, whichever comes first
Interactive Hover/Tap: Counts, Avatars, and Actions
Given a viewer interacts with any active segment of the band When hovering (desktop) or tapping (mobile) Then a popover shows total member count, up to the first 10 avatars with an overflow indicator, and action buttons And the Check-In button is enabled only if the viewer is currently in-window and opens the check-in flow within 300 ms And the React action posts a reaction within 1 second and updates counts without a full refresh And the popover is dismissible via outside click/tap or Esc and respects a 44x44 px minimum touch target on mobile
Progressive Loading and Batching for Large Rooms
Given a room with 5,000–10,000 members When the activity band initially loads Then initial skeleton UI renders within 500 ms and first meaningful paint of the band occurs within 1,000 ms on a mid-tier mobile device And member detail data loads in batches of up to 50 members per request with no more than 5 concurrent requests And the UI remains responsive (main thread long tasks < 200 ms) and memory growth is bounded without crashes during continuous updates for 10 minutes
Accessibility and Inclusive Interaction
Given keyboard and screen reader users navigate the band When tabbing through interactive elements Then focus order is logical, visible focus indicators are present, and Enter/Space activate the popover; Esc closes it And screen readers announce segment role, current in-window count, ending-soon count, and forecast labels with ARIA-compliant semantics And all text and indicators meet a minimum 4.5:1 contrast ratio and respect reduced motion settings
Host Prompt Guidance and Integration
Given a host is viewing the room When the Guidance overlay is toggled on Then the band shows "In Window Now" and "Ending Soon (<=15m)" counts, plus the next forecast peak window within 2 hours And the host can send or schedule a prompt from the overlay, with prompts rate-limited to 1 per 60 seconds per room And all actions are logged to analytics with room ID, timestamp, counts, and outcome, and do not reveal member-specific times to the host
Check-in Validation & Streak Logic
"As a user, I want my check-in to be accepted only when it falls within my window and update my streak accordingly so that my progress remains accurate and fair."
Description

Validates one-tap check-ins against the current rolling window, accepting eligible actions, rejecting out-of-window attempts with clear feedback, and idempotently handling retries. On acceptance, updates streak count, rolls the window anchor, and emits analytics events; on misses, applies defined rules (e.g., no backdating, clear miss markers) to preserve fairness. Covers edge cases such as back-to-back check-ins, offline mode synchronization, and race conditions. Provides auditable logs for moderation and debugging and integrates with reactions and notifications for immediate feedback.

Acceptance Criteria
Accept In-Window Check-In and Update Streak
Given a user U with a configured rolling window length L (20–28 hours) and current active window W = [A, A + L) And U has not yet checked in during W When U taps Check In at server time t such that A ≤ t < A + L Then persist a single check-in record for U in room R with timestamp t and idempotencyKey K And set U's next window to W' = [t, t + L) And if the immediately previous window was not missed, set new_streak = prior_streak + 1; otherwise set new_streak = 1 And publish event "check_in.accepted" to reactions/notifications and update the group activity band to reflect U's state And emit analytics event "check_in_accepted" including {userId, roomId, t, L, prior_streak, new_streak} And return HTTP 200 with {checkInId, timestamp: t, streak: new_streak, windowStart: t, windowEnd: t + L}
Reject Out-of-Window Check-In with Clear Feedback
Given a user U with active window W = [A, A + L) When U attempts Check In at server time t where t < A or t ≥ A + L Then do not create a check-in record And do not modify streak or window anchor And return HTTP 409 with error_code = "OUT_OF_WINDOW" and a message including the next window start time and time-remaining And publish event "check_in.rejected" with reason = "out_of_window" for moderation visibility And emit analytics event "check_in_rejected_out_of_window" including {userId, roomId, t, L, windowStart: A, windowEnd: A + L}
Idempotent Retries and Back-to-Back Within Same Window
Given U has an accepted check-in in the current window W with idempotencyKey K When the client retries the same request with idempotencyKey K within 10 minutes Then return HTTP 200 with the original {checkInId, timestamp, streak, windowStart, windowEnd} And do not create an additional record And do not change streak or window anchor Given U has already an accepted check-in in W When U attempts another check-in in W using any idempotencyKey Then return HTTP 409 with error_code = "ALREADY_CHECKED_IN" And do not change streak or window anchor And emit analytics event "check_in_duplicate_same_window" including {userId, roomId, windowStart: A, windowEnd: A + L}
Offline Check-In Queue, Backfill, and Conflict Rules
Given U is offline and taps Check In at device time td with idempotencyKey K, which is queued with createdAt = td When connectivity is restored and the client syncs Then process queued check-ins in chronological order, validating each against server-computed windows using createdAt timestamps And accept a queued item only if its createdAt falls within the computed current window at the time of processing; otherwise reject with reason = "stale_or_out_of_window" And for each accepted queued item, persist with serverReceivedAt, set new window to [createdAt, createdAt + L], and update streak per rules (increment if no miss since prior acceptance; else set to 1) And for each rejected queued item, do not modify streak or window anchor and record rejection reason And if an online acceptance advanced the window past a queued item's createdAt window, reject that queued item with reason = "window_advanced" And return per-item results to the client (200 for accepted; 409 with error details for rejected) And emit analytics events "check_in_offline_accepted" or "check_in_offline_rejected" with {userId, roomId, createdAt, serverReceivedAt, reason}
Concurrent Requests Race Condition Resolution
Given two or more check-in requests for the same user U and room R arrive concurrently (distinct idempotency keys) When the server processes these requests Then enforce single-writer semantics via a versioned window anchor or distributed lock And allow exactly one request to succeed for the current window And return HTTP 200 only for the winning request; all others receive HTTP 409 with error_code = "CONFLICT_WINDOW_ADVANCED" or "ALREADY_CHECKED_IN" And update streak and window anchor exactly once And emit one "check_in.accepted" event and analytics for the winner, and "check_in.rejected" events with conflict reasons for the losers And ensure no duplicate check-in records are created
Miss Detection, No Backdating, and Miss Markers
Given a window W = [A, A + L) elapses with no accepted check-in for user U When the system evaluates streak state Then mark W as missed and append a miss marker for W in U's audit history And set streak = 0 upon miss; on the next accepted check-in set streak = 1 And do not allow any subsequent check-in to be applied to W (no backdating), including offline items whose createdAt ∈ W that arrive after a later window has been accepted And emit analytics event "check_in_missed" with {userId, roomId, windowStart: A, windowEnd: A + L}
Audit Logging and Analytics Emission
Given any check-in attempt (accepted or rejected) is processed When the decision is finalized Then write an immutable audit log entry containing {decision, reason, userId, roomId, idempotencyKey, serverTime, requestTime (if provided), windowStart, windowEnd, prior_streak, new_streak, source (online|offline), requestId} And make audit logs queryable by moderators by {userId, roomId, timeRange} within 1 second of write And ensure all acceptance/rejection paths emit analytics events with consistent schemas and no PII And redact or hash idempotency keys and user messages in analytics payloads
Host In-Window Roster
"As a host, I want a real-time roster of who is currently in-window or ending soon so that I can time prompts and reactions for maximum engagement."
Description

Provides hosts a real-time roster that segments members into in-window, ending-soon, and out-of-window groups with counts, filters, and sort options. Enables quick actions to nudge, mention, or react to members likely to engage now. Respects privacy by surfacing status without exposing exact window boundaries. Updates live via subscriptions and scales for large rooms through server-side pagination and delta streaming. Integrates with the activity band and notification system for coordinated engagement.

Acceptance Criteria
Real-Time In-Window Segmentation and Counts
Given a host opens the roster for a room When the roster loads Then members are grouped into In-Window, Ending-Soon, and Out-of-Window with accurate count headers Given a member’s rolling window state changes (enters window, becomes ending-soon, or exits window) When the change is received by the client subscription Then the member’s segment and the corresponding counts update within 2 seconds at p95 with no duplicate entries Rule: In-Window is computed server-side using each member’s configured rolling window length (20–28 hours) and current server time; Ending-Soon uses the room’s threshold; Out-of-Window is everyone else Rule: No member appears in more than one segment simultaneously
Engagement Filters and Sort Controls
Given the host applies a Status filter (In-Window, Ending-Soon, Out-of-Window) When the filter is toggled Then only members matching the selected statuses are shown and the list updates without full reload Given the host selects a sort option When sorting by Status, Last Activity, Streak Length, or Display Name Then results are ordered accordingly; ties break by member_id ascending; the selection persists for the session Rule: Filters and sorts are applied server-side and respected by pagination and delta updates
Quick Actions: Nudge, Mention, React
Given a member is In-Window or Ending-Soon When the host taps Nudge Then a nudge notification event is sent via the notification system, the action returns success, and the roster shows a transient “Nudged” indicator for up to 5 minutes Given the host taps Mention on one or more selected members When the composer opens Then the selected @mentions are prefilled and sending posts to the room feed without leaving the roster context Given the host taps React for a member When a reaction is selected Then the reaction posts to the room activity band and is attributed to the host Rule: Nudge and Mention are rate-limited to 1 per member per 10 minutes; exceeding the limit returns 429 and shows a cooldown message Rule: Actions are disabled for Out-of-Window members
Scalable Pagination and Delta Streaming
Given a room with up to 10,000 members When the host opens the roster Then the first page (50 members) loads under 800 ms p95 after network response, and additional pages load under 400 ms p95 Given the host scrolls to request the next page When the server returns data with a cursor Then results append without gaps or duplicates and maintain the active sort order Given live deltas arrive while pagination is active When a member’s state changes Then the item is re-positioned or removed/inserted in-place idempotently using sequence IDs, without flicker Rule: Subscription reconnect applies missed deltas via backfill using the last acknowledged cursor/sequence
Privacy-Preserving Status Only
Rule: The roster displays only status badges (In-Window, Ending-Soon, Out-of-Window) and coarse activity indicators; no exact window start/end times, countdowns, or timestamps are shown Given a host inspects a member row When viewing tooltips or details Then no exact window boundaries, timezone, or last-check-in timestamps are revealed Rule: Network responses to the host client omit exact window boundary fields; only boolean/bucketed status is transmitted
Activity Band Integration and Sync
Given the host taps the activity band’s “Now” segment When navigating to the roster Then the roster auto-filters to In-Window and the count matches the activity band Given a member’s state changes When both the band and roster are visible Then both update within 2 seconds p95 and remain count-consistent Given the host schedules or sends a prompt from the activity band When switching to the roster actions Then the same targeted set (In-Window at action time) is preselected for mentions
Flex Prompt Scheduling
"As a host, I want prompts to auto-schedule when most members are in-window so that engagement increases without spamming."
Description

Introduces an auto-scheduler that times room prompts when a configurable threshold of members are in-window, using rolling forecasts from member windows and historical engagement. Supports quiet hours, per-room defaults, and host overrides. Minimizes notification fatigue by batching and throttling prompts and using relevance scoring. Exposes experimentation hooks (A/B) and metrics for prompt effectiveness. Integrates with push notifications and the activity band to deliver well-timed engagement nudges.

Acceptance Criteria
Threshold-Based Auto-Prompt Trigger
Given a room with 10 members and threshold_type = "percent" and threshold_value = 60 and prompt_dispatch_sla = 2 minutes, And 6 members are currently in-window, When the scheduler runs, Then it queues one prompt for the room within 60 seconds and delivers it within 2 minutes. Given threshold_type = "count" and threshold_value = 3, And at least 3 members are currently in-window, When the scheduler runs, Then exactly one prompt is dispatched and no duplicate prompt is dispatched for the room within the next 15 minutes. Given the threshold is not met, When the scheduler runs, Then no prompt is dispatched.
Rolling Window Forecast Scheduling
Given forecast_horizon = 60 minutes and rolling in-window forecasts indicate the threshold will be met at T+27 minutes, When the scheduler evaluates at time T, Then it schedules the prompt for T+27 minutes (±1 minute) and does not schedule any earlier prompt for the same room. Given forecast data is older than 10 minutes or unavailable, When the scheduler evaluates, Then it falls back to current in-window state without scheduling based on stale forecasts. Given members' windows update due to recent check-ins, When the scheduler evaluates, Then it recomputes forecasts within 60 seconds and adjusts any future scheduled prompt to the earliest time that still meets the threshold.
Quiet Hours and Host Override Respect
Given a member has quiet_hours = 22:00-07:00 local, When a prompt is dispatched at 23:00, Then no push notification is sent to that member during quiet hours, and the room activity band entry is still created at dispatch time. Given a host taps "Send Now" override, When invoked, Then a prompt is posted to the room within 60 seconds regardless of threshold or forecast state, and push notifications still respect each member's quiet hours and notification opt-out. Given a room-level prompt_hours window of 08:00-22:00 is configured, When the scheduler evaluates at 07:30, Then it does not dispatch an auto-prompt until 08:00.
Per-Room Default Configuration Management
Given a new room is created, When no overrides are set, Then default scheduler settings are applied: threshold_type = "percent", threshold_value = 50, forecast_horizon = 60 minutes, batch_window = 3 minutes, per_user_max_prompts_24h = 2. Given a host with manage permissions updates threshold_type/value and prompt_hours via room settings, When saved, Then the changes take effect within 5 minutes and are audit-logged with actor, old_value, new_value, and timestamp. Given a non-host member attempts to update scheduler settings, When attempted, Then the change is rejected with 403/permission_denied and no configuration is altered.
Batching, Throttling, and Relevance Scoring
Given two prompts would be dispatched within 3 minutes for the same room, When the scheduler processes them, Then they are batched into a single activity band prompt and a single push per eligible member. Given per_user_max_prompts_24h = 2 for a room, When a member has already received 2 push prompts in the last 24 hours, Then further pushes are suppressed while the activity band prompt is still created. Given relevance_score is computed per member [0..1] and min_relevance_threshold = 0.6 and max_push_recipients = 5, When a prompt is dispatched to 20 members, Then only members with score >= 0.6 up to the top 5 by score receive pushes; others do not receive pushes but see the activity band prompt.
Push and Activity Band Delivery with Idempotency
Given a prompt is dispatched with prompt_id, When delivery occurs, Then exactly one activity band entry is created per room keyed by prompt_id and multiple delivery retries do not create duplicates. Given a push is sent to iOS/Android with a deep link to the room and prompt_id, When the member taps the push, Then the app opens directly to the room with the prompt scrolled into view and highlighted. Given a transient push delivery failure occurs, When retries are attempted, Then the system retries up to 3 times over 5 minutes and records success/failure per attempt.
Experimentation Hooks and Effectiveness Metrics
Given an active A/B test with experiment_id and variants A and B, When a member becomes eligible for a prompt, Then the member is assigned to a stable variant for that room and an exposure event {experiment_id, variant, user_id, room_id, prompt_id, timestamp} is logged before any prompt is sent. Given a prompt lifecycle, When events occur, Then the system logs prompt_scheduled, prompt_dispatched, push_sent, push_delivered, push_opened, prompt_viewed, check_in_within_30m, and reaction_added with timestamps, room_id, user_id, prompt_id, experiment_id/variant_id, and relevance_score. Given an analytics consumer queries metrics, When requesting per-room and per-variant KPIs, Then CTR, open-to-check-in conversion within window, time-to-engagement, and lift vs control are available for the last 30 days.
Time Zone & DST Resilience
"As a traveling user, I want my rolling window to work correctly across time zones and DST so that my streak isn’t unfairly penalized."
Description

Ensures window calculations are duration-based and stored in UTC so they remain stable across time zone changes and daylight saving transitions. Accurately renders local times per user while keeping eligibility logic time-zone agnostic. Handles travel scenarios, device time drift, and DST boundary conditions without unfairly shortening or extending windows. Provides monitoring and alerts for anomalous time data and offers user-facing explanations when local displays differ from UTC-based logic.

Acceptance Criteria
UTC Durational Window Stability Across Time Zone Changes
Given a user in a Room Flex Sync room with rolling window duration D in [20h, 28h] and a last successful check-in at T0 (UTC) When the user changes device time zone or locale Then eligibility is computed as [T0, T0 + D) in UTC and remains exactly D in length (±1s) And the user's in-window/out-of-window status does not change due to zone/locale changes alone And the host "in-window now" roster is unchanged by zone/locale changes alone And all persisted check-in and window timestamps are stored as ISO 8601 UTC
DST Spring Forward Gap Handling Without Window Loss
Given D in [20h, 28h] and T0 (UTC) and the user's locale enters DST with a missing hour between T0 and T0 + D in local time When local wall-clock advances by 1 hour (spring forward) Then eligibility remains open until UTC reaches T0 + D And the local UI displays the window end as convertUTC(T0 + D) with a DST indicator And a check-in after the skipped local hour but before UTC T0 + D is accepted
DST Fall Back Repeat Hour Handling Without Window Extension
Given D in [20h, 28h] and T0 (UTC) and the user's locale leaves DST causing a repeated hour between T0 and T0 + D in local time When local wall-clock repeats an hour (fall back) Then eligibility closes at UTC T0 + D (no extension) And the local UI disambiguates repeated times using offset or labels (e.g., 01:30 (UTC−5) vs 01:30 (UTC−4)) And check-ins within the repeated hour are accepted only if UTC ∈ [T0, T0 + D)
Device Time Drift Detection and Server-Time Fallback
Given the device clock offset from server time is >120 seconds When the user attempts a check-in Then server time is used as the authoritative timestamp for eligibility And a non-blocking warning is shown explaining server time is used and the measured offset And an anomalous_time_offset event is logged with user_id, room_id, device_offset_seconds, and timestamp And attempts to backdate or future-date beyond the offline policy are rejected with an explanation
Cross-Time-Zone Travel Eligibility Consistency
Given a user checked in at T0 (UTC) in time zone A and D in [20h, 28h] When the user opens the room in time zone B before UTC reaches T0 + D Then the user is marked "in-window now" in the host roster if and only if current UTC < T0 + D And last check-in and window end are displayed in time zone B local time converted from UTC And no eligibility change occurs solely due to the time zone change
Operational Monitoring and Alerting for Time Anomalies
Given the system receives time-related telemetry When within any 15-minute window one or more occurs: device_offset_seconds >300 for ≥0.5% of check-ins, client timezone changes >3 per user, or missing/invalid timezone reported by ≥50 clients Then an alert is emitted to monitoring with severity=warning and includes metric, threshold, counts, and sample IDs And anomalies are recorded on a dashboard with time series and room/user counts And no user-facing notifications are sent as part of the alert
Offline Check-in Backfill with UTC Consistency
Given a user is offline and taps check-in When connectivity is restored within 10 minutes Then the server assigns the official check-in timestamp as min(server_receipt_time, last_seen_server_time + 10 minutes) in UTC And eligibility is evaluated with that UTC timestamp against [T0, T0 + D) And if outside the window the check-in is rejected with a clear explanation and a link to "Why different from my local time?" And if accepted the local UI displays the check-in time converted to the user's current time zone
Fair-Play Guardrails
"As a host, I want safeguards that prevent users from gaming flex windows so that room streaks remain trustworthy."
Description

Implements safeguards to prevent gaming of rolling windows: no backdating, no overlapping or queued check-ins, minimum elapsed time enforcement, and detection of device time tampering or suspicious patterns. Provides clear user feedback on rejected actions, flags anomalies for host moderation, and maintains an immutable audit trail. Integrates with the validation layer and analytics to ensure streak integrity across rooms while maintaining a low-friction check-in experience.

Acceptance Criteria
No Backdating Check-ins
Given a user is in a shared room with a personal rolling window [windowOpenAt, windowCloseAt] computed on the server When they attempt to submit a check-in with any client-supplied timestamp earlier than serverNow minus 5 minutes allowableClockSkew Then the server uses serverNow as the authoritative timestamp And rejects the check-in if serverNow < windowOpenAt And returns HTTP 422 with errorCode "CHECKIN_BACKDATE_BLOCKED" and nextEligibleAt = windowOpenAt (ISO-8601 UTC) And records an audit entry with status "rejected" reason "backdate" linked to attemptId And does not change the user's streak or the room's activity band
Single Check-in per Rolling Minimum Window (No Overlap/Queue)
Given the user has an accepted check-in at T1 When they attempt another check-in while serverNow < T1 + minElapsedHours (default 20h; room-configurable 18–22h) Then the attempt is rejected with HTTP 422 errorCode "CHECKIN_MIN_ELAPSED_NOT_MET" and nextEligibleAt = T1 + minElapsedHours And the streak and activity band remain unchanged And an audit record is written with status "rejected" reason "min_elapsed" Given multiple check-in requests are received within 5 seconds for the same user and room with the same idempotencyKey When the first request is processed Then subsequent requests return the same result with deduplicated = true And no queued check-ins are created or stored
Minimum Elapsed Time Calculation Is Timezone/DST Safe
Given a user changes device timezone or a DST transition occurs between attempts When computing nextEligibleAt and evaluating eligibility Then the server measures elapsed time strictly in UTC as (serverNow - lastAcceptedAt) >= minElapsedHours And nextEligibleAt is returned as ISO-8601 UTC And device timezone or DST does not affect acceptance or rejection
Device Time Tampering Detection and Mitigation
Given the client's deviceClock deviates from serverNow by > 5 minutes or moves backward > 1 minute compared to last seen client timestamp When a check-in attempt is received Then eligibility is evaluated using serverNow only And the audit record flags deviceTimeSuspect = true with deviceTimeDriftSeconds recorded And the API includes warningCode "DEVICE_TIME_SUSPECT" in the response if drift > 5 minutes And no rejection occurs solely due to drift unless another guardrail is violated
Suspicious Pattern Detection and Host Flagging
Given pattern detection rules are enabled When any of the following thresholds are met within a rolling 7-day window: - 5 or more accepted check-ins occur within the last 60 seconds of the user's window - 3 or more rejections occur with reason "min_elapsed" - 2 or more attempts are flagged deviceTimeSuspect Then the user receives status "suspect" for that room And the host dashboard shows a Guardrail Flag badge within 60 seconds containing ruleIds and timestamps And an audit record "flag_raised" is appended with the triggering evidence And clearing the flag requires explicit host action "flag_cleared" which is also audited
Clear User Feedback on Rejected Actions
Given a check-in attempt is rejected by any guardrail When the API responds Then it returns HTTP 422 with JSON fields: errorCode, message, reasonDetails, nextEligibleAt (if applicable), attemptId (all required) And the client displays an inline banner within 500 ms containing the human-readable reason and, if provided, a one-tap action to set a reminder at nextEligibleAt And the banner can be dismissed and does not block future eligible check-ins
Immutable Audit Trail and Analytics Integration
Given any check-in attempt (accepted or rejected) When the attempt is processed Then an append-only audit record is written containing: attemptId, hashedUserId, roomId, serverTimestamp, clientTimestamp, decision (accepted/rejected), reasonCode, nextEligibleAt (if any), deviceTimeDriftSeconds, previousRecordHash, recordHash And records form a verifiable hash chain where SHA-256(previousRecordHash + recordPayload) == recordHash And the validation layer is invoked synchronously and returns decisionId referenced in the audit record And acceptance (HTTP 200) is returned only after the audit write succeeds with idempotent retries And analytics events "checkin_attempted" and "checkin_decided" are emitted with non-PII metadata within 2 seconds of decision And guardrail validation plus audit logging adds ≤ 100 ms to P95 check-in latency in production

Pseudonym Picker

Choose a unique alias per room with smart suggestions and collision checks. One-tap regenerate, lock, or auto-rotate for public rooms to stay fresh. Keeps your presence recognizable in a room without exposing your identity, reducing setup friction and letting you join fast with confidence.

Requirements

Room-Scoped Unique Alias Generation
"As a member joining a new room, I want an alias that is unique in that room so that I can be recognized there without revealing my identity."
Description

Generate and validate pseudonyms that are unique within each room while remaining unlinkable across rooms. Enforce allowed character sets, length limits, case-insensitive comparisons, and reserved-word exclusions. The server is the source of truth for uniqueness and persistence, with optimistic UI feedback and graceful offline fallback to a temporary alias that reconciles on reconnect. On confirmation, the chosen alias is stored with the user’s room membership record and broadcast to presence streams. Integrates with Room, Identity, and Presence services; emits metrics for suggestion acceptance rate, collision rate, and error rates to monitor quality and performance.

Acceptance Criteria
Server-Enforced Room-Scoped Uniqueness and Validation
Given a user proposes an alias for Room A When the server validates the alias Then the alias must match allowed characters [A-Za-z0-9_-] and length between 3 and 24 inclusive And the alias must not match any reserved word (e.g., admin, mod, system) And the server compares aliases case-insensitively and with NFC normalization for uniqueness And if invalid, the server responds 400 validation_error with a specific code and human-readable message And if unique and valid, the server persists the alias on the room-membership record and returns success And if already claimed in Room A, the server responds 409 alias_conflict and returns at least 3 alternative suggestions
Optimistic Alias Suggestion with Server Confirmation and Race Handling
Given the client displays a suggested alias as Available based on a local pre-check When the user taps Confirm Then the client shows a pending state and blocks duplicate submissions until the server responds And no presence event is emitted client-side before server confirmation When another user claims the same alias first and the server returns 409 alias_conflict Then the client surfaces a clear conflict message and replaces the suggestion list within 500 ms of the response And the client remains in selection state without applying the losing alias When the server confirms the alias Then the client applies the alias, exits pending state, and shows success within 200 ms
Offline Temporary Alias Fallback and Reconciliation on Reconnect
Given the device is offline and the user joins a room When the user selects or accepts a suggested alias Then the client generates a locally unique temporary alias that satisfies format rules and marks it as temporary And the UI labels the alias as Offline (temp) When connectivity is restored Then the client attempts to claim the temporary alias on the server And if accepted, the alias is marked permanent and the Offline (temp) label is removed And if 409 alias_conflict or validation_error occurs, the client auto-regenerates and retries up to 3 times before prompting the user And throughout, the user remains in the room; presence is updated only after successful server confirmation
Per-Room Alias Persistence and Cross-Room Unlinkability
Given a user has a confirmed alias in Room A When they rejoin Room A on a new session or device Then the same alias is loaded from the room-membership record without re-selection Given the same user joins Room B When choosing an alias for Room B Then any valid alias may be chosen regardless of Room A's alias, and uniqueness is checked only within Room B And presence/events sent to other clients include only room-scoped membership_id and alias (display + canonical) without a global user identifier And no event or API response exposes a stable cross-room identifier that links the user's aliases
Presence Broadcast and Visibility After Alias Confirmation
Given the server confirms an alias set or change When the Presence service publishes updates Then all current room members receive a single alias_set or alias_changed event within 2 seconds And new entrants after the change see the current alias in the initial presence snapshot And events include membership_id, alias_display (preserved case), alias_canonical (case-folded), version/etag, and occurred_at And no duplicate events are emitted for the same version
Smart Suggestions, One-Tap Regenerate, Lock, and Auto-Rotate for Public Rooms
Given the pseudonym picker is opened When suggestions are requested Then at least 3 suggestions are presented that pass validation rules and are not reserved words And suggestions are unique within the current room at time of generation When the user taps Regenerate Then a fresh set of suggestions is displayed without duplicates from the last set Given Auto-rotate is enabled for a public room and the alias is not locked When the rotation interval elapses (room-configurable; a default exists) Then the server assigns a new unique alias, persists it, and Presence broadcasts the change Given the user enables Lock When auto-rotate would occur Then the alias does not change until unlocked
Metrics Emission for Alias Quality and Performance
Given suggestions are shown to a user When the UI renders the suggestion list Then the system increments suggestion_shown with room_id, platform, and generation_source labels When a user confirms a suggestion Then suggestion_accepted is incremented, enabling acceptance_rate = accepted/shown per room and overall When a 409 alias_conflict occurs Then collision_rate is incremented with room_id and endpoint labels When 4xx/5xx errors occur on alias endpoints Then invalid_rate or error_rate counters are incremented accordingly, and latency histograms record p50/p95/p99 And metrics are visible on the dashboard with filters for room visibility (public/private), client version, and platform
Smart Contextual Suggestions
"As a user, I want smart alias suggestions that fit the room’s vibe so that I can pick something quickly without thinking too hard."
Description

Provide fast, context-aware alias suggestions that match the room’s theme and the user’s preferred style (e.g., professional, playful). Generate a batch of options with one-tap refresh, deduplicate within a session, and respect language/locale settings. Suggestions avoid sensitive topics, NSFW terms, and cultural pitfalls, and can include optional emojis where permitted. Ensure sub-150ms generation/render on median devices, cache recent suggestions, and track acceptance to continuously improve the model. Integrates with Localization, Safety, and Analytics systems.

Acceptance Criteria
Context + Style Aligned Suggestions on First Open
Given a room with theme metadata available and a user preference set to a style (e.g., Playful) When the user opens the Pseudonym Picker for that room Then the system displays 10 unique alias suggestions in a single batch And at least 80% of suggestions score >=0.7 on both theme-alignment and style-alignment classifiers And no suggestion contains the user’s real name, username, or email substring And suggestions include 0–1 emoji each only if the room policy permits emojis
One‑Tap Refresh With Session‑Level Deduplication
Given a new Pseudonym Picker session When the user taps Refresh up to 5 times Then each refreshed batch contains 10 suggestions with 0 duplicates within the batch And no suggestion string repeats across batches within the same session until at least 50 unique suggestions have been shown And the UI updates within a single frame after the refresh action completes
Locale/Language Respect and Emoji Policy Compliance
Given the device/app locale is set (e.g., fr-FR) and the room policy disallows emojis When the user opens the Pseudonym Picker Then 100% of suggestions are rendered in the target locale language/script and contain 0 emojis And diacritics and locale-specific casing are preserved Given the locale is set to ja-JP and the room policy allows emojis When suggestions are generated Then suggestions use Japanese scripts and include at most 1 emoji per suggestion in no more than 50% of suggestions
Safety Guardrails: No Sensitive/NSFW/Cultural Pitfalls
Given Safety filtering is enabled for the room When a batch of 10 suggestions is generated Then 0 suggestions are flagged as high severity by the Safety service (NSFW, hate, slurs, self-harm, extremist content) And 0 suggestions match the product profanity/blocklist dictionary And a QA spot-check sample of 500 suggestions from public rooms contains 0 critical violations and <=0.2% minor warnings
Performance: Sub‑150ms Generation and Render
Given a median device (e.g., Pixel 6a, iPhone 12) on a typical network When the user opens the Pseudonym Picker or taps Refresh Then time-to-first-meaningful-paint for the suggestions list is <=150 ms at p50 and <=300 ms at p95 (telemetry: alias_suggestions_ttfmp_ms) And CPU usage stays below 30% average during generation/render for 200 ms windows And no dropped frames >5% during the interaction (telemetry: jank_rate)
Caching and Acceptance Tracking for Continuous Improvement
Given a user has generated suggestions for a room When the user returns to the same room within 24 hours Then cached recent suggestions are used to meet performance targets and previously shown suggestions are de-prioritized and not repeated until 50 unique suggestions have been shown And an Analytics event alias_suggestion_impression is logged for each suggestion with fields {room_id, locale, style, suggestion_id, position} And when the user accepts a suggestion, event alias_suggestion_accepted is logged with {room_id, locale, style, suggestion_id} and contains no PII And 99% of analytics events are delivered within 5 minutes (telemetry: analytics_delivery_latency_ms)
Resilient Integrations: Localization, Safety, Analytics
Given third-party or internal services (Localization, Safety, Analytics) are reachable When generating a batch Then the system invokes Localization for language assets once per session, filters all candidates via Safety pre-publish, and emits Analytics events for impression, refresh, and accept Given any one integration is degraded or times out (>=100 ms Safety, >=100 ms Localization, >=500 ms Analytics) When generating a batch Then the system falls back to a local, safe suggestion list and still meets the Performance acceptance criteria at p50 And a non-blocking error is logged with correlation id and surfaced as a silent metric without user-facing interruption
Live Availability Check & Short-Hold Reservation
"As a user typing my own alias, I want to know instantly if it’s available so that I don’t waste time and avoid conflicts."
Description

Validate user-typed aliases in real time with latency-tolerant UX and clear availability indicators. When an alias is selected, place a short TTL server-side reservation to prevent race conditions during join/confirm flows. Handle case-insensitive and Unicode confusable variants to avoid lookalike collisions. Provide immediate alternatives on conflict and retry seamlessly. Rate-limit checks to protect back-end resources and surface telemetry on conflicts, holds, and timeouts. Expose dedicated API endpoints for check and reserve, with idempotency and traceability.

Acceptance Criteria
Real-time Alias Availability Indicator During Typing
Given a user is entering an alias in the Pseudonym Picker for a specific room When the user pauses typing for at least 300 ms Then exactly one availability check is sent for the current alias text with normalization (Unicode NFKC + casefold) and the roomId, including a correlationId Given a check request is in flight When the user changes the alias text Then the prior response is ignored and no UI state changes unless it matches the latest alias text Given a check response arrives within 2 seconds Then the UI shows one of: Available, Unavailable, or Held (by you or by another), with icon and accessible text Given a check response does not arrive within 2 seconds or times out at 5 seconds Then the UI shows a non-blocking Checking… state, does not block typing, and a retry occurs on the next pause or blur
Casefolding and Confusable Collision Prevention
Given the room already contains alias "Alice" When a user enters "alice" or "ALICE" Then availability is Unavailable with reason collision_casefold and the normalizedAlias is "alice" Given the room already contains alias "tom" When a user enters "tоm" where the "o" is Cyrillic U+043E Then availability is Unavailable with reason collision_confusable Given the room already contains alias "Max" When a user enters "Μax" where "M" is Greek Mu U+039C Then availability is Unavailable with reason collision_confusable Given the room has no aliases similar to "bae" When a user enters "bæ" or "BÄE" Then the check returns Available if their confusable skeletons do not match any existing alias and the normalizedAlias is returned
Short TTL Reservation During Join/Confirm Flow
Given the latest availability check returns Available for an alias When the user taps Continue or Reserve Then the client calls POST /v1/alias/reserve with idempotencyKey and correlationId and receives 201 with reservationId and ttlSeconds=30 Given a reservation was created Then the UI shows a visible countdown equal to ttlSeconds, labeled Held, and disables alias edits for that reservation Given another client checks the same alias during the hold Then they receive status HeldByOther with holdExpiresAt Given the user confirms within ttlSeconds When the client confirms with reservationId Then the alias is assigned to the user, the hold is closed, and subsequent checks return TakenByYou for that user Given ttlSeconds elapse without confirmation Then the reservation is released server-side and a subsequent confirm returns 409 ReservationExpired
Conflict Handling with Immediate Alternatives and Seamless Retry
Given a reserve or confirm attempt fails due to collision or race Then the UI immediately shows at least 3 alternative suggestions that are pre-validated as Available and not confusable with existing aliases Given the user taps Regenerate Then a new set of at least 3 unique suggestions appears within 300 ms Given the user selects any suggestion Then a reserve attempt is triggered automatically; on success, the user proceeds without retyping Given a network failure during retry Then the UI preserves the typed alias, surfaces a retry action, and does not lose the previous suggestions list
Rate Limiting and Client Backoff for Availability Checks
Given a user is typing in the alias field Then the client debounces checks by at least 300 ms and coalesces rapid changes into one request Given the server enforces rate limits When more than 5 check requests are received within 1 second for the same user and room Then the server responds 429 with a Retry-After header and the request is counted without hitting the alias store Given the client receives a 429 Then it suppresses further checks for the Retry-After duration (minimum 2 seconds), displays a non-blocking Too many checks notice, and resumes checks after the backoff
Dedicated Check/Reserve Endpoints with Idempotency and Traceability
Given the client calls POST /v1/alias/check with {roomId, aliasRaw} and correlationId Then the response is 200 with {normalizedAlias, status in [Available, Unavailable, HeldByOther, TakenByYou], reason?, holdExpiresAt?} and an echoed correlationId Given the client calls POST /v1/alias/reserve with {roomId, normalizedAlias, idempotencyKey} and correlationId Then the response is 201 with {reservationId, ttlSeconds >= 20 and <= 60, expiresAt} and an idempotency fingerprint Given the same idempotencyKey is retried within the TTL Then the server returns the original reservationId and identical ttlSeconds/expiresAt Given any request is processed Then correlationId and request metadata are written to logs/telemetry for traceability, including outcome and latency
Hold Release on Edit, Cancel, or Navigation Away
Given a reservation is active for an alias When the user edits the alias, cancels the flow, or navigates away from the room join Then the client requests release of the hold if supported or drops its reservation, and the server frees the alias immediately or at TTL expiry Given a hold is released Then the check API reflects the alias as Available within 1 second Given the user returns within 10 seconds after cancel Then previous suggestions are restored but the previous reservationId is not reused
One-Tap Regenerate, Select, and Lock Controls
"As a returning participant, I want to lock my alias once I find one I like so that it stays consistent across sessions."
Description

Offer intuitive controls to regenerate suggestions, select an alias, and lock it to prevent future changes or auto-rotation. Persist lock state per room and surface it in the UI with clear iconography and tooltips. Support undo/unlock, keyboard navigation, and haptic/accessibility feedback. Sync selections across devices immediately and handle failures with retry and user-friendly messages. Emit analytics events for regenerate, select, lock, and unlock to inform UX tuning.

Acceptance Criteria
One-Tap Regenerate Updates Suggestions
Given the user is viewing the Pseudonym Picker for a room with a visible suggestions list When the user taps the Regenerate control once Then a new set of suggestions replaces the current list within 300 ms And no suggestion duplicates any currently visible suggestion And the previously selected alias (if any) remains selected and pinned And the Regenerate control shows a transient loading/disabled state that clears within 500 ms And an analytics event "alias_regenerate" is emitted with room_id, source="button", suggestion_count
One-Tap Select Alias Persists Per Room
Given suggestions are visible and the alias is not locked When the user taps a suggestion once Then that alias becomes the active alias for this room with immediate UI acknowledgement (<100 ms) And the active alias is persisted to the backend scoped to this room and user And the room header/avatar chip updates to reflect the selected alias And an analytics event "alias_select" is emitted with room_id, alias_id, is_locked=false And if persistence fails, a non-blocking error toast appears and the client retries up to 3 times with 1s, 2s, 4s backoff; on final failure the UI reverts to the previous alias
Lock Alias Prevents Changes and Auto-Rotation
Given the user has an active alias in the room When the user taps the Lock control once Then the alias enters a Locked state indicated by a lock icon next to the alias And a tooltip on hover/focus reads "Locked: alias will not auto-rotate or change until you unlock" And regenerate and auto-rotation are disabled while locked; attempts surface a tooltip/toast "Alias is locked" And the lock state is persisted per room and restored on app relaunch And an analytics event "alias_lock" is emitted with room_id, alias_id
Undo and Unlock Flows
Given the user has just performed Select or Lock When an inline Undo action is shown for 5 seconds Then tapping Undo within that window reverts to the prior alias and lock state within 200 ms And an analytics event "alias_undo" is emitted with room_id, action_type Given the alias is Locked When the user taps Unlock once Then the alias becomes Unlocked; regenerate and selection controls re-enable And an analytics event "alias_unlock" is emitted with room_id, alias_id
Keyboard Navigation and Shortcuts
Given the user is on desktop/web with keyboard focus within the Pseudonym Picker When the user presses Tab/Shift+Tab Then focus moves through Regenerate, suggestion options, Lock/Unlock, and Undo (if present) with a visible focus outline When a suggestion option has focus and the user presses Enter or Space Then that alias is selected as if tapped When the Lock/Unlock control has focus and the user presses Space Then the lock state toggles accordingly And interactive elements expose appropriate ARIA roles and labels (e.g., listbox/option/button)
Haptic and Accessibility Feedback
Given the device supports haptic feedback When the user selects, locks, or unlocks an alias Then a light impact haptic feedback triggers once per action Given a screen reader is active When regenerate, select, lock, unlock, or undo occur Then a concise announcement is spoken (e.g., "Alias locked") and the lock control reflects state via aria-pressed And tooltips are keyboard and screen-reader accessible and dismiss with Esc or loss of focus
Cross-Device Sync and Failure Handling
Given the user has two active sessions for the same account in the same room When the user selects, locks, unlocks, or undoes an alias on one session Then the other session reflects the change within 1.5 seconds at the 95th percentile And if syncing fails due to network loss, the local UI shows a non-blocking "Syncing..." badge and retries with exponential backoff for up to 5 minutes, then displays "Tap to retry" And no duplicate analytics events are emitted for a single action; clients de-duplicate using a shared action_id And on recovery, final state matches the last successfully applied action order across sessions
Auto-Rotate for Public Rooms
"As a privacy-conscious user in public rooms, I want my alias to auto-rotate on a schedule so that I stay fresh and harder to track."
Description

Enable optional scheduled alias rotation for unlocked aliases in public rooms to reduce tracking and keep identities fresh. Provide room-level and user-level settings for cadence (e.g., daily, weekly), quiet hours, and opt-out. Preserve recognizability via stable visual accents or a stem-plus-variant pattern while ensuring new variants pass safety filters and uniqueness checks. Notify users prior to rotation with snooze options, execute rotations atomically with collision handling, and log all changes for auditability. Implement as an idempotent background job with backoff and observability.

Acceptance Criteria
Room and User Settings Precedence with Quiet Hours
Given a user belongs to a public room with auto-rotate enabled and both user-level and room-level cadence are set When the scheduler evaluates rotation time Then the room-level cadence overrides the user-level cadence for that room Given quiet hours 22:00–07:00 in the user's timezone and a rotation is due at 23:00 When the scheduler evaluates Then the rotation is deferred to 07:00 local time Given a room-level timezone is configured When computing cadence boundaries and quiet hours Then that timezone is used; otherwise the user's timezone is used Given the next eligible rotation time falls on a cadence boundary within quiet hours When the scheduler runs Then the rotation occurs at the first minute after quiet hours end
Alias Eligibility: Public Room, Unlocked, Opt-Out Respected
Given a room is private When the scheduler evaluates Then auto-rotate is not scheduled or executed for that room Given a user has locked their alias in a public room When a rotation window occurs Then the alias is not rotated Given a user has opted out of auto-rotate for a public room When a rotation window occurs Then the alias is not rotated and any pending notifications for that rotation are canceled Given a public room and an unlocked alias and auto-rotate enabled When a rotation window occurs Then the alias is marked eligible for rotation
Atomic Rotation with Uniqueness Collision Handling
Given a new alias variant collides with an existing alias in the room When generating a candidate Then the system regenerates up to 5 times before applying a deterministic disambiguator suffix to ensure uniqueness Given a rotation is executed When updating alias text, accent, and indexes Then all updates commit atomically in a single transaction or none commit Given two rotations for the same room run concurrently When uniqueness is enforced Then no two users end up with the same alias due to a room-scoped unique constraint Given rotation events are streamed to subscribers When a rotation commits Then only the final committed alias is emitted; no intermediate values are visible
Recognizability via Stable Accent or Stem-Plus-Variant
Given recognizability_mode=accent for a room When rotations occur Then the user's visual accent_id remains unchanged across rotations in that room Given recognizability_mode=stem_variant for a room and a user's stem is "Aurora" When rotations occur Then the displayed alias follows "Aurora-<variant>" and the stem "Aurora" remains constant while the variant changes each rotation Given recognizability_mode is changed for a room When the next rotation occurs Then only the invariants for the active mode are enforced and prior mode invariants are not applied
Safety Filters on Generated Alias Variants
Given a generated alias candidate When evaluated against safety filters Then it must not match profanity, hate, NSFW, PII (email, phone, address), or banned brand lists (case- and locale-insensitive) Given a candidate fails a safety filter When generating the next candidate Then the reason is recorded and a new candidate is generated until a safe candidate is found or 5 attempts are exhausted Given 5 failed attempts occur When the rotation would execute Then the rotation is skipped, an audit entry with reason=filtered is recorded, and the user is notified that the rotation was skipped
Pre-Rotation Notification with Snooze Options
Given a scheduled rotation When pre-notification lead time is configured (default 10 minutes) Then a notification is sent at least that many minutes before rotation Given quiet hours are in effect during the pre-notification window When the notification would fall within quiet hours Then the notification is withheld and delivered at quiet-hours end with a minimum 1-minute delay before rotation Given the user taps Snooze 1 hour on the notification When the snooze is applied Then the rotation is deferred by 1 hour subject to quiet hours and a new notification is scheduled Given the user taps Snooze to next window When applied Then the current rotation is skipped and the next cadence window outside quiet hours is targeted Given notifications are disabled by the user When a rotation occurs Then the rotation proceeds without sending notifications
Idempotent Background Job with Backoff, Observability, and Audit Logs
Given the rotation job is triggered multiple times for the same user-room-window When idempotency is enforced Then only one rotation occurs using a deduplication key of user_id+room_id+window_start Given a transient failure occurs (e.g., timeout, 5xx) When retrying Then exponential backoff with jitter is used with retries capped at 1 hour total delay; permanent validation errors are not retried Given observability requirements When the system runs Then metrics counters and timers are emitted for rotations_attempted, rotations_succeeded, rotations_failed, retries, collisions_resolved, and skipped_due_to_lock/opt_out, with trace/span correlation via request_id Given any rotation attempt (success, skip, or failure) When logging Then an immutable audit record is written containing job_id, request_id, user_id, room_id, old_alias, new_alias (if any), old_accent, new_accent (if any), recognizability_mode, result, reason, and timestamp, and is queryable by user_id, room_id, and date range
Safety, Impersonation, and Profanity Filters
"As a community member, I want unsafe or deceptive aliases to be blocked so that rooms feel respectful and trustworthy."
Description

Apply multi-language safety filters to both generated and user-entered aliases, blocking profanity, slurs, hate speech, sexual content, personal identifiers (email/phone), and brand/public figure impersonation. Detect Unicode homographs/confusables and prevent deceptive lookalikes. Provide clear, respectful error messages with safe alternatives. Maintain room-level allow/deny lists and admin overrides, with regular list updates and performance budgets that support real-time checks. Store moderation decisions and signals for audits and continuous improvement.

Acceptance Criteria
User-Entered Alias Safety Filtering (Multilingual)
Given a user enters an alias in any supported language or script When the alias contains profanity, slurs, hate speech, or sexual content per current policy Then the alias is rejected and not saved, and the user sees a respectful message with 5 safe alternatives Given a user enters an alias containing obfuscated variants (leetspeak, spacing, diacritics) When normalization is applied Then the content safety check evaluates both raw and normalized forms and blocks if either is unsafe Given classifier confidence is below 0.6 for a potentially unsafe term When the decision is ambiguous Then the system fails closed and treats the alias as unsafe, offering alternatives and guidance to try again Given the alias is safe When the user submits Then the alias is accepted and stored for the room
Generated Alias Suggestions Safety Filtering
Given the system generates up to 20 alias suggestions When any suggestion violates safety policy Then it is filtered out before display and replaced until at least 6 safe suggestions are shown or a 700 ms timeout is reached Given fewer than 3 safe suggestions are available by the timeout When the timeout occurs Then show an error state with a one-tap Regenerate action and do not display unsafe suggestions Given a user taps a displayed suggestion When confirming the alias Then the suggestion is revalidated and accepted only if still safe
Personal Identifiers (Email/Phone) Detection and Blocking
Given a user-entered alias or generated suggestion contains an email address or phone number, including obfuscated forms (e.g., "name at domain dot com", spaces, Unicode substitutions) When validated Then the alias is rejected and the message explains that personal contact info is not allowed without echoing the detected value Given a partially redacted email/phone pattern that still enables contact When validated Then the alias is rejected Given an alias that includes numbers unrelated to contact info (e.g., "Runner42") When validated Then the alias is accepted if no other violations exist
Unicode Homograph and Confusables Defense
Given a proposed alias contains mixed scripts or Unicode confusables When compared using confusable-skeleton mapping (UTS #39 or equivalent) after stripping zero-width characters and normalizing to NFC Then reject the alias if its skeleton equals an existing alias skeleton in the room or reserved terms Given a proposed alias uses mixed Latin/Cyrillic/Greek letters to mimic ASCII names When validated Then the alias is rejected as deceptive Given a proposed alias uses a single non-Latin script legitimately without deception When validated Then it is allowed if it passes other checks
Brand/Public Figure Impersonation Prevention
Given a proposed alias matches or closely resembles (edit distance <= 1 or confusable-skeleton equal) a protected brand or public figure name/handle in the blocklist When validated Then reject the alias and communicate that impersonation is not allowed Given a proposed alias includes restricted qualifiers such as "official", "admin", "moderator", or "support" suggesting authority When validated Then reject the alias unless the user holds the corresponding role in that room and an admin explicitly allows it Given a proposed alias that claims parody with qualifiers like "not the real" When validated Then it is still rejected if it matches or closely resembles a protected entity
Respectful Error Messaging with Safe Alternatives
Given an alias is rejected for any reason When the error is shown Then the message uses a neutral tone, references the reason category (e.g., personal info, impersonation, language), does not echo the blocked text, is localized to the device language, and meets WCAG AA contrast Given alternatives are displayed When generating suggestions Then provide 5 safe alternatives that are semantically distinct from each other and from the blocked alias Given the user selects Regenerate When regeneration occurs Then a new set of alternatives is produced with at least 80% new items compared to the last set
Room-Level Lists, Admin Overrides, Performance, and Audit Logging
Given a room deny list entry exists When an alias matches the entry (exact, prefix/suffix, or regex as configured) Then the alias is rejected even if it passes global checks Given a room admin grants an override to allow a flagged alias When the override is saved Then the alias becomes allowable only in that room, the override records admin ID, reason, timestamp, and optional expiry, and the decision is included in audit logs Given global safety lists are updated When a new version is published Then clients/services apply updates within 24 hours for routine updates and within 2 hours for hotfixes, and the active version is recorded with each decision Given an alias is validated in-room When measuring performance Then p95 end-to-end validation latency is <= 80 ms and p99 <= 150 ms, with a fail-closed behavior on timeouts > 300 ms Given any moderation decision is made When storing logs Then store hashed/redacted alias, normalized skeleton, decision, reason codes, language detection, list/model versions, latency, room ID, pseudonymous user ID, and override flag for 90 days, exportable to admins on request
Cross-Room Identity Isolation & Analytics Anonymization
"As a privacy-conscious user, I want my alias in one room to be untraceable to aliases in other rooms so that my activity remains compartmentalized."
Description

Guarantee that pseudonyms are compartmentalized per room and never exposed or inferable across rooms. Store per-room alias identifiers and use salted hashes for metrics so analytics can track feature health without linking user identity or cross-room behavior. Prevent suggestion models from leaking cross-room data and display clear privacy copy in the picker. Conform to data retention and export policies by allowing users to view/download room-level pseudonym history without mapping to their real identity.

Acceptance Criteria
Per-Room Pseudonym Isolation (No Cross-Room Linkage)
Given the same account joins Room A and Room B and selects the alias text "NightOwl" in both, When any member views profiles or participant lists in either room, Then no identifier, link, or UI element indicates the alias is shared across rooms. Given the client requests GET /rooms/{room_id}/members, When inspecting the JSON response, Then it includes alias and room_alias_id only and excludes account_id, global_user_id, email, or identifiers from other rooms. Given the same account is present in two rooms, When comparing room_alias_id values returned by each room's membership API, Then the values differ and are unique per (account_id, room_id). Given a user searches for aliases by text, When using any in-app search, Then results are scoped to the current room only and never include matches from other rooms. Given server access logs for the past 24 hours, When filtering by request/response payloads, Then no single log line includes room_alias_id values from more than one room for the same account.
Room-Scoped Data Model and API Constraints
Given the database schema, When inspecting the room_aliases table, Then a unique constraint exists on (account_id, room_id) and room_alias_id is an opaque UUID v4 unique per row. Given an API consumer calls GET /rooms/{room_id}/members and GET /rooms/{other_room_id}/members for the same account, When comparing the responses, Then the room_alias_id values are different and there is no key present to join identities across rooms. Given an API consumer calls any endpoint outside a room scope to resolve an alias to an account, When executed, Then the request is rejected with 404/403 and returns no data mapping alias to account. Given a client attempts to query rooms by alias text across all rooms, When executed, Then the API returns 400 indicating the operation is unsupported.
Analytics Anonymization with Per-Room Salted Hashes
Given analytics events are emitted for alias interactions, When inspecting the event payload, Then it contains anon_user_key computed as a salted hash derived from room_alias_id and a per-room salt, and contains no account_id, email, device_id, or IP. Given the same account generates analytics events in Room A and Room B, When comparing anon_user_key values, Then they differ across rooms. Given events within the same room during a salt rotation window, When comparing anon_user_key values, Then they are stable and consistent for cohorting within that room. Given salts are rotated on the defined schedule, When the rotation completes, Then previous salts are destroyed within 30 days and are not exportable to analytics consumers. Given an attempt to join event streams across rooms on anon_user_key, When executed on a validation dataset (>10k events), Then zero cross-room joins are produced.
Suggestion Model Cross-Room Data Leak Prevention
Given a user creates a unique alias "BlueMango27" in Room A that is not in the global dictionary, When opening the pseudonym picker in Room B, Then "BlueMango27" does not appear in the top 20 suggestions or as a personalized suggestion. Given the personalization feature is enabled, When reviewing the model feature inputs, Then no per-user cross-room alias history, room_ids other than the current, or account_id are present. Given the training data pipeline executes, When validating datasets, Then only room-local aggregates are used for suggestions and no aggregate with count < 25 is included; raw alias_text from other rooms is never exposed to end users. Given live suggestion-serving telemetry, When sampling requests, Then zero requests include headers or parameters containing account_id or cross-room identifiers.
Privacy Copy Display in Pseudonym Picker
Given a user opens the pseudonym picker in any room for the first time in a session, When the UI renders, Then a privacy notice appears inline within 250 ms stating that aliases are room-specific, not linked across rooms, and analytics are anonymized. Given the privacy notice is displayed, When measured, Then the copy length is <= 240 characters, reading grade <= 8, includes a tappable Learn more link to the Privacy Policy, and is localized for all supported languages. Given accessibility requirements, When navigating with a screen reader, Then the privacy notice is announced with role=note and the link is focusable and operable. Given the info icon next to the alias field, When tapped, Then a modal shows the same privacy copy and can be dismissed via ESC/back.
Room-Level Pseudonym History View and Export (No Real-Identity Mapping)
Given a signed-in user navigates to Settings > Privacy > Pseudonym History, When the list loads, Then it shows entries per room with room_name, alias_text, and active date range without displaying email, full name, or account_id. Given the user requests an export, When the file is generated, Then the export (JSON or CSV) contains room_id, room_name, alias_text, room_alias_id (opaque), created_at, ended_at and excludes real-identity fields (email, phone, legal name, IP, device IDs). Given an export link is generated, When time passes, Then the link expires within 7 days and the file is deleted from storage upon expiry. Given the user deletes their account or requests data deletion, When the deletion job completes, Then the mapping from account_id to room_alias_id is removed within 30 days while room_alias_id remains only as an orphaned identifier in room content with no way to re-link to the user. Given a user attempts to upload or import the export back into the app, When executed, Then the system rejects any operation that would map the export contents to a global identity.

Avatar Veil

Swap personal photos for generative, mask-style avatars seeded to your pseudonym. Consistent within a room, distinct across rooms, and lightly animated for status (e.g., streak glow) without revealing anything personal. Express yourself safely while remaining easy to spot in busy rooms.

Requirements

Pseudonym-Seeded Avatar Generator
"As a privacy-conscious user, I want my avatar to be automatically generated from my pseudonym so that I can be recognized without revealing personal information."
Description

Implements a deterministic avatar generator that produces mask-style, non-photographic avatars from a salted hash of the user’s pseudonym and room ID. The generator outputs vector-based, GPU-friendly assets with parameterized shapes, textures, and color palettes to ensure uniqueness without personal data. It exposes a platform-agnostic service that returns avatar parameters and animation states, not raw images, enabling consistent rendering on iOS, Android, and web. It includes a versioned seed schema, collision detection to avoid lookalike avatars within a room, and regeneration rules when pseudonyms change. Expected outcome: visually distinctive, safe avatars that replace profile photos across all places where avatars appear (rooms list, roster, reactions, check-in feed).

Acceptance Criteria
Cross-Platform Determinism and Consistency
Given P="pseudonym123", R="roomA", V="v1", S="saltX" When generateAvatar(P,R,V,S) is called from iOS, Android, and Web Then each response contains identical parameter JSON (order-insensitive) with seedSchemaVersion="v1" and the same checksum And repeated calls (100x per platform) yield the identical checksum And changing any single input among {P,R,V,S} changes the checksum and at least one of {shapeFamily, maskPattern, colorPaletteId}
In-Room Uniqueness and Collision Resolution
Given a room with N=100 pseudonyms and fixed V,S When generating avatars for all users Then no two avatars share the tuple (shapeFamily, maskPattern, primaryPaletteId) And all pairwise similarityScore < 0.25 using similarityFunction v1 And upon initial collision the generator retries up to K=5 re-seeds; if still colliding, it forces a non-conflicting colorPaletteId and sets metadata.collisionResolved=true
Cross-Room Distinctness with Pseudonym Consistency
Given the same pseudonym P in rooms R1 != R2 with same V,S When generating both avatars Then checksums differ and at least two of {shapeFamily, maskPattern, colorPaletteId} differ And for repeated generations in the same room with unchanged inputs, parameters and checksum remain identical
Versioned Seed Schema and Client Pinning
Given versions v1 and v2 are available When the client requests generateAvatar(..., V="v1") Then the response has seedSchemaVersion="v1" and the checksum equals the previous v1 checksum When the client omits V Then the response uses the latest version and may produce a different checksum When the app is upgraded but continues requesting V="v1" Then the response remains identical When V="v1" is deprecated Then the service returns a deprecation error code and does not silently switch versions
Animation State Parameters for Status Events
Given status=idle When generating Then animationStates.streakGlow.enabled=false and amplitude=0 Given status=streak_active with streakLength>=3 When generating Then animationStates.streakGlow.enabled=true and amplitude in [0.2,0.6] and periodMs in [1200,2000] Given event=check_in When generating Then animationStates.checkInBurst.enabled=true and durationMs in [400,800] and particleCount in [4,12] And all animation parameters are numeric scalars or arrays of length<=4
Vector Output Contract and Privacy Constraints
When generating Then the response contains no bitmap/image bytes and includes assetType="vector" And the payload defines <=40 paths, each with <=64 segments and <=4 gradient stops, and total distinct colors <=12 And the 95th percentile compressed payload size is <=6 KB (gzip) across 10,000 samples And request inputs are limited to {pseudonym, roomId, seedSchemaVersion?, status flags}; any attempt to upload personal images is rejected with 4xx
Regeneration on Pseudonym Change and Surface Coverage
Given a user in room R changes pseudonym from P1 to P2 When generating with P2 Then the checksum differs from the P1 checksum and collision detection is re-applied until non-conflicting And within 60 seconds of the change, avatars in rooms list, room roster, reactions, and check-in feed reflect the new avatar And no personal photo is rendered on any surface
Room-Scoped Consistency and Cross-Room Distinction
"As a member of multiple rooms, I want my avatar to stay the same within each room but be different across rooms so that I’m easy to spot without linking my identity across contexts."
Description

Ensures each user has a consistent avatar within a given room while appearing differently in other rooms. Uses a stable room-scoped seed (pseudonym + room salt) and a global salt rotation policy. Maintains continuity during sessions and across devices; if a pseudonym changes, a migration layer persists the prior avatar until the user confirms a refresh. Provides APIs to fetch the room-specific avatar signature, resolves collisions within a room, and guarantees that users never share indistinguishable silhouettes or palettes in the same room.

Acceptance Criteria
Consistent Avatar Within a Room Across Sessions and Devices
Given a user with pseudonym P is a member of room R under global salt version G When the user views their avatar in room R on multiple devices and across app restarts without G or P changing Then the avatar_signature, silhouette_id, and palette_id are identical across devices and sessions And the deterministic animation seeded by avatar_signature produces the same animation_hash across devices (allowing frame-rate variance only) And the avatar bytes or sprite hash for the base frame are equal across devices
Distinct Avatars Across Different Rooms
Given a user with pseudonym P is a member of rooms R1 and R2 with distinct room salts When the user views their avatars in R1 and R2 under the same global salt version Then the avatar_signature values differ between R1 and R2 And at least one of silhouette_id or palette_id differs between R1 and R2 And the avatars are not flagged as visually-indistinguishable by the similarity check (similarity_score < threshold)
Collision-Free Avatars Within a Room
Given a room R with existing members When a new avatar is generated for a joining or updating user Then no two users in room R share the same (silhouette_id, palette_id) tuple And if a collision is detected, the resolver applies a deterministic perturbation to produce a unique tuple in no more than 3 attempts And the collision resolution completes within 300ms at P95 under expected room load
API: Fetch Room-Specific Avatar Signature
Given a valid auth token and membership in room R for user U When GET /rooms/{roomId}/avatars/{userId} is called for U in R Then the response is 200 with JSON containing avatar_signature, silhouette_id, palette_id, salt_version, pseudonym_version, and ETag And when called with If-None-Match matching the current ETag, the response is 304 without a body And unauthorized requests return 401, and requests for non-member or unknown IDs return 404 And the endpoint responds within 150ms at P95 under normal load
Global Salt Rotation Respects Session Continuity
Given the platform rotates the global salt at time T When a user session started before T remains active Then all avatar renders in that session continue using the pre-rotation salt until the session ends And the first new session started after T derives avatars with the new global salt and remains consistent across devices thereafter And API responses after rotation include updated salt_version and ETag to trigger client cache invalidation
Pseudonym Change Migration With User-Confirmed Refresh
Given a user changes their pseudonym from P_old to P_new When the user re-enters any room R Then the avatar based on P_old remains in effect and avatar_signature is unchanged until the user explicitly confirms a refresh And upon confirmation, the avatar is re-derived from (P_new + room_salt) and the new avatar_signature propagates to all devices within 5 seconds And if the user cancels or closes the prompt, the prior avatar persists across sessions until a refresh is later confirmed
Real-Time Uniqueness During Simultaneous Joins
Given multiple users join room R within a 2-second window When their avatars are generated concurrently Then the system prevents rendering duplicates by reserving uniqueness for (silhouette_id, palette_id) tuples or reconciles any race within 500ms via update events And after reconciliation, all clients display unique avatars for all users with no remaining duplicates
Status-Aware Micro-Animations
"As an active participant, I want my avatar to subtly show my streak and actions so that others can notice my activity at a glance without being distracted."
Description

Adds lightweight, non-distracting animation states to avatars that reflect real-time status: streak glow intensity for streak length, pulse on live check-in, ripple on reaction, and subtle idle breathing. Animations are capped at 60fps with power-saving fallbacks at 30fps, adhere to reduced-motion OS settings, and pause in background. Exposes a small set of animation tokens (e.g., streak_level, is_live, reacted_recently) from the activity service to the renderer. Integrates with the reactions pipeline and check-in events, and provides accessibility-compliant alternatives.

Acceptance Criteria
Streak Glow Intensity Scales with Streak Length
- Given streak_level ∈ {0,1,2,3,4,5} from the activity service, when the avatar renders in a room, then glow intensity maps as: 0→0%, 1→20%, 2→35%, 3→50%, 4→70%, 5→90% (±5% tolerance). - Given streak_level=0, then no glow animation is rendered (static, no pulsing). - When streak_level changes, then the glow intensity transitions complete in 150–250ms with a smooth ease curve and no overshoot. - When reduced motion is enabled, then intensity changes occur as instant state updates or cross-fades ≤100ms with no continuous motion.
Live Check-in Pulse Animation Semantics
- Given a successful local check-in event, then the avatar plays one pulse animation of 600ms duration, peak scale ≤1.08x, and does not repeat unless another check-in occurs. - Given a remote user’s check-in event is received, then their pulse begins within 300ms of event receipt. - While a pulse is active, idle breathing is suppressed for that avatar. - If reduced motion is enabled, then the pulse is replaced by a non-motion highlight ≤100ms with no scale change.
Reaction Ripple Animation and Throttling Behavior
- Given a reaction event targeting an avatar, then a ripple plays once (duration 500ms), expanding to avatar bounds +6px with opacity fading to 0 by end of animation. - Given multiple reactions within any 2s window, then at most 2 ripples are rendered, spaced ≥300ms apart (excess reactions are visually aggregated). - The first ripple starts within 300ms of the first reaction event receipt. - Ripple opacity never obscures more than 20% of the avatar’s visible area at any time. - If reduced motion is enabled, then ripple is replaced with a brief color/opacity highlight ≤100ms and no expansion.
Idle Breathing Subtle Motion Defaults
- When no pulse, ripple, or glow transition is active for ≥2s and reduced motion is disabled, then idle breathing runs: scale oscillation amplitude ≤2%, period 3s ±0.5s, continuous. - Idle breathing pauses immediately when any higher-priority animation (pulse/ripple/glow transition) starts and resumes ≥1s after the last event completes. - Idle breathing never triggers when reduced motion is enabled.
Performance Caps and Lifecycle Pause/Resume
- The animation scheduler never requests frames above 60fps (verified via instrumentation). - When OS battery saver is active or rolling average FPS over the last 1s drops below 55, then all animations switch to ≤30fps within 500ms and stay until conditions clear for ≥5s. - When the app is backgrounded, room is not visible, or avatar cell is offscreen, then all avatar animations pause within 100ms and no animation frames are scheduled until visibility returns. - On resume/visibility, animations continue from current state without replaying queued animations (no catch-up effects).
Respect Reduced Motion and Accessibility Alternatives
- When OS reduced motion is enabled, then continuous animations (idle breathing, glow transitions) are disabled; state changes use non-motion alternatives (opacity/color step or cross-fade ≤100ms). - Event animations (pulse/ripple) are replaced with non-motion highlights ≤100ms and do not move/scale. - Each avatar exposes accessible status text updates on changes (e.g., "Streak day N", "Live check-in", "New reactions"), debounced to ≤1 announcement per 5s per avatar, with polite priority. - No animation or highlight exceeds 3 flashes per second or violates platform luminance/contrast safety guidelines.
Animation Token Contract and Update Propagation
- The activity service exposes to the renderer the tokens: streak_level (integer 0–5), is_live (boolean), reacted_recently (ISO 8601 timestamp or null); tokens are room-scoped per user. - Token updates propagate to the renderer within ≤200ms of source events; on room join/subscribe, initial token values are available within ≤500ms. - On room leave or app background, reacted_recently expires within 5s and tokens reset to defaults: streak_level=0, is_live=false, reacted_recently=null. - If tokens are missing/malformed, the renderer uses safe defaults (no animations) and logs a single recoverable error per session.
Distinctiveness in Busy Rooms
"As a user in large rooms, I want avatars that are easy to tell apart so that I can quickly find people without scanning names."
Description

Optimizes avatar designs for crowded interfaces with high visual salience and accessibility. Implements a color-blind–safe palette, strong edge contrast, and unique silhouette rules. Includes a collision-avoidance algorithm that maximizes perceptual distance between avatars in the same room. Provides a 'busy room' rendering mode that increases outline thickness and reduces animation amplitude when more than twelve avatars are visible. Supplies tooltip initials on hover for web and compact badges on mobile for ambiguity resolution.

Acceptance Criteria
Edge Contrast Baseline Compliance
Given any avatar rendered at 24–72 px on standard or dark backgrounds When the avatar is displayed next to at least one other avatar Then the visible edge stroke shall maintain a luminance contrast ratio ≥ 3:1 against any adjacent avatar fill And the edge stroke shall maintain a contrast ratio ≥ 4.5:1 against the room background And the minimum stroke thickness shall be ≥ 1 px at 1x scale and scales proportionally with device pixel ratio And automated visual tests at 100%, 150%, and 200% zoom shall pass these thresholds
Color-Blind–Safe Palette Compliance
Given the app’s approved avatar palette and textures When simulated under protanopia, deuteranopia, tritanopia, and achromatopsia Then any two avatars in the same room shall have a simulated color distance CIEDE2000 ≥ 20 And no pair shall map to indistinguishable hues in the simulations And all status accents (e.g., streak glow) shall retain a contrast ratio ≥ 3:1 against the avatar’s base color under each simulation
Silhouette Uniqueness Enforcement
Given a room with N visible avatars (up to 50) When avatars are generated or a new participant joins Then no two visible avatars shall share the same silhouette category And the minimum shape descriptor distance between any two silhouettes shall be ≥ 0.20 (cosine distance on the v1 shape vector) And conflicts are resolved deterministically within 200 ms using the room seed And if three attempts fail, apply a high-contrast pattern overlay to one conflicting avatar
Perceptual Distance Collision Avoidance
Given all visible avatars in a room When the set of avatars changes due to join/leave or viewport resize Then the minimum combined perceptual distance score (color + silhouette + texture), PD_v1, between any two avatars shall be ≥ 0.75 And if PD_v1 < 0.75 for any pair, the system shall reassign appearance features to restore PD_v1 ≥ 0.75 within 200 ms And after resolution, PD_v1 ≥ 0.75 holds for 99% of pairs in a 1,000-room simulation at 50 avatars/room
Busy Room Rendering Mode Threshold & Behavior
Given a viewport where 13 or more avatars are visible simultaneously When the 13th avatar becomes visible or the viewport resizes to reveal ≥ 13 avatars Then Busy Room Mode activates within 100 ms And avatar outline stroke thickness increases by ≥ 1.5× baseline (min +1 px at 1x DPI) And animation amplitude reduces to 40% ± 5% of baseline while preserving animation timing And when visible avatars drop to ≤ 12, Busy Room Mode deactivates within 200 ms without visual artifacts (no flicker > 50 ms)
Web Tooltip Initials on Hover
Given a desktop web client and a pointing device When the cursor hovers over an avatar for ≥ 200 ms Then a tooltip appears centered above the avatar within 150 ms displaying the avatar’s 2–3 character initials And the tooltip meets WCAG AA contrast (≥ 4.5:1) and is accessible via aria-describedby And the tooltip hides within 150 ms after hover ends and does not occlude more than 20 px of neighboring avatars
Mobile Compact Badges for Ambiguity
Given an iOS or Android client in a room with ≥ 13 visible avatars or with any pair PD_v1 < 0.85 When avatars are rendered in the grid/list Then a compact initials badge (10–12 px cap height) appears on avatars flagged as ambiguous And badge text meets ≥ 4.5:1 contrast and remains legible at 320 px viewport width And a long-press on any avatar toggles its badge for 2 seconds even if not ambiguous And badges do not overlap adjacent avatars or critical UI affordances
Privacy-First Data Handling
"As a privacy-focused user, I want assurance that my avatar cannot be traced back to me so that I feel safe participating."
Description

Eliminates personal photo storage and prevents reidentification. Stores only nonreversible seeds and avatar parameter sets; no raw images or EXIF or biometric data are collected. Uses one-way hashing with per-room salts and key rotation; prohibits cross-room linkage via backend queries. Provides privacy mode by default, opt-in reroll controls with rate limits, and transparent privacy copy. Includes security reviews, threat modeling for linkage attacks, and telemetry only at aggregate levels.

Acceptance Criteria
No Personal Photo Storage
- Given any client flow related to avatars or profiles When a user attempts to upload a personal photo Then the backend rejects the request with HTTP 400 and message "Photo uploads are not supported" - Given production data stores (DB, object storage, CDN cache) When scanned by automated inventory jobs daily Then the count of image/* MIME objects equals 0 and no columns containing EXIF or biometric fields exist - Given request/response and application logs When scanned with DLP rules for image signatures and EXIF keys Then zero matches are found - Given a GDPR/CCPA data export for a user When generated Then it contains only nonreversible seeds and avatar parameter sets and no raw images or EXIF metadata
Per-Room Nonreversible Seed Derivation
- Given a user with the same pseudonym joins Room A and Room B When avatar seeds are generated Then seed_hash_A != seed_hash_B and each remains stable across sessions within its room - Given the seed derivation function When inspected Then it uses Argon2id with parameters m >= 64MB, t >= 3, p >= 1 and a 128-bit salt unique per room - Given the storage schema When queried Then only seed_hash, room_id, room_user_id, and avatar_params are stored together and no raw pseudonym or global user_id resides in the same record - Given an offline attack with 10^9 guesses on common identifiers When executed against stored seed_hashes Then success rate is 0% due to one-way KDF and per-room salts - Given a versioned server-side pepper When rotated Then avatar_params and on-screen avatars remain unchanged within each room and all hashes are re-derived without user-visible impact
Backend Cross-Room Linkage Prevention
- Given an internal query attempting to join room_user records across rooms When executed Then it is denied by row-level security/policy with an audit entry containing user, query_id, timestamp - Given an internal API call to resolve a global identity from room-specific identifiers When no break-glass approval is present Then the call is rejected with HTTP 403 and is logged - Given a privacy penetration test on a 10k synthetic dataset When cross-room linkage attacks are attempted Then achieved precision <= 5% and recall <= 5% at 95% confidence - Given the privacy threat model document When reviewed in security council Then all High/Medium risks for linkage attacks are marked mitigated or accepted with justification and target dates
Privacy Mode Default Onboarding
- Given a new user completes onboarding and joins their first room When settings are initialized Then Privacy Mode is ON by default - Given Privacy Mode is ON When viewing member lists and profiles Then only pseudonym and generative avatar are shown; no email, real name, or external IDs are rendered - Given default telemetry settings When events are produced Then only aggregate counters are emitted and no user-level event stream is stored - Given Settings > Privacy is opened on a fresh account When displayed Then the toggle is ON with label "On by default" and a link to the privacy policy is present and functional
Opt-in Avatar Reroll With Rate Limits
- Given a user taps Reroll in a room and is within limit When processed Then a new avatar is generated and persisted, replacing the prior avatar for that room - Given a user attempts more than 3 rerolls within 24 hours in the same room When the 4th request occurs Then the backend returns HTTP 429 with retry-after and the UI shows "Reroll limit reached" - Given reroll actions When viewed in metrics Then per-user-per-room reroll_count_24h is available and alerting thresholds are configured (P95 > 2.5 triggers warning) - Given audit logging When queried for a user and room Then reroll events include room_id, room_user_id, timestamp, actor=user, result, and request_id
Transparent Privacy Copy Surfaces
- Given the first avatar generation in a room When completed Then a modal displays privacy copy stating "No personal photos stored," "No EXIF/biometrics collected," "Per-room pseudonymous avatars," "Aggregate-only telemetry" with links to policy - Given a privacy-impacting action (reroll, join room, toggle privacy) When triggered Then an inline notice appears with a "Learn more" link to /privacy that opens successfully - Given analytics on privacy copy When measured over 7 rolling days Then >= 95% of first-time room joins see the copy and average dwell time is >= 3 seconds - Given accessibility checks When run on the privacy copy surfaces Then contrast ratio >= 4.5:1 and screen reader labels read the core statements
Aggregate-only Telemetry Enforcement
- Given event generation for Avatar Veil When data is sent Then only aggregated counters/histograms are stored; no per-user identifiers or room_user_id are persisted in telemetry tables - Given an analyst attempts to query user-level telemetry When executed Then the query is blocked by data governance policies and access is logged - Given telemetry schemas When inspected Then all tables include a data_retention_days <= 90 and no columns named user_id, email, pseudonym, room_user_id, or device_id exist - Given a privacy audit job running weekly When executed Then it verifies k-anonymity k >= 50 for all released aggregates and reports pass/fail in a dashboard
Performance and Caching Pipeline
"As a mobile user, I want avatars to load instantly and smoothly so that the app feels responsive even on poor connections."
Description

Delivers fast avatar rendering under 100 ms P95 on repeat views. Implements server-side parameter generation with client-side vector renderers and LRU caches, prewarms caches for recent room participants, and uses CDN caching for parameter payloads keyed by room and user. Provides offline fallbacks with last-known parameters and a deterministic local generator seed to avoid flicker. Includes monitoring for render time, payload size under two kilobytes on average, and animation frame drops.

Acceptance Criteria
P95 Repeat-View Render Latency Under 100 ms
Given a user has previously viewed avatars in a room and their parameter payloads are cached client-side or at the CDN When the user revisits the same room within the cache TTL and avatars are requested for display Then the client renders each avatar in <= 100 ms at the 95th percentile measured from render request to first painted frame across >= 1,000 repeat views on a mid-tier device cohort And no network round trip blocks rendering when a cache hit occurs And instrumentation records render_time_ms with a repeat_view=true tag
Parameter Payload Size Budget and Compression
Given avatar parameter payloads are generated server-side and served via CDN with compression When clients fetch parameters for a sample of >= 10,000 deliveries across active rooms Then the mean payload_size_bytes is <= 2,048 bytes and the 95th percentile is <= 4,096 bytes And payloads contain only vector/parametric data (no raster images or textures) And schemaVersion and avatarVersion fields are present to support cache invalidation
CDN Caching Keyed by Room+User with Prewarm
Given a user opens a room with >= 20 recent participants When the client triggers prewarm requests for the latest parameter payloads for the most recent 50 participants Then within the next 10 minutes, at least 90% of those payload requests return X-Cache=HIT and Age>0 at the CDN And the cache key includes roomId, userId, and avatarVersion And origin request rate for those payloads is reduced by >= 80% compared to a no-prewarm baseline
Client LRU Cache Correctness and Eviction
Given the client LRU cache is configured with a capacity of 200 avatar parameter entries per room When a user cycles through three busy rooms causing more than 200 unique avatars per room to be rendered Then the cache evicts the least-recently-used entries deterministically without cross-room contamination (keys include roomId+userId+avatarVersion) And cache hits serve parameters without network access And no incorrect avatar is displayed during or after eviction events
Offline Fallback Deterministic Rendering Without Flicker
Given the device loses network connectivity while viewing a room When an avatar needs to be rendered Then if last-known parameters exist locally, the avatar renders from those parameters without error And if no parameters exist, a deterministic local avatar is generated from a seed derived from pseudonym and roomId and remains stable across app restarts And upon reconnect, if server parameters match, the avatar remains visually identical; else a cross-fade transition of <= 200 ms updates to the server version without flicker
Telemetry and Alerting for Render, Payload, and Frame Drops
Given production traffic over a rolling 7-day window When telemetry is collected for avatar rendering Then dashboards report P95 repeat render time, payload size averages and percentiles, client/CDN cache hit rates, and animation frame drops per 10 seconds And alerts fire within 5 minutes if P95 repeat render time > 100 ms for 3 consecutive intervals or mean payload size > 2,048 bytes for 3 consecutive intervals or frame drop rate > 2% on the mid-tier cohort And alert destinations are configured and receive test pages
Animation Smoothness Under Load
Given the streak glow animation is active for avatars in a room When observed for 10 seconds on supported mid-tier devices after warm cache Then the animation runs at 60 fps with dropped frames <= 2% and no single frame stutter exceeding 50 ms And frame drop metrics are emitted per avatar instance with device tier tags
Admin Moderation and Abuse Controls
"As a moderator, I want to address avatar-related abuse without exposing identities so that rooms remain safe and compliant."
Description

Equips moderators with tools compatible with anonymity. Supports per-room avatar disable or override when necessary, reroll on abuse reports, and audit logs without exposing user personally identifiable information. Adds rate limits to avatar rerolls, blocks offensive custom names from influencing seeds, and provides a safe list of palettes and shapes that avoids harmful symbolism. Integrates with the existing report flow and ensures actions propagate to all clients in the room in real time.

Acceptance Criteria
Per-room Avatar Disable Control
Given a moderator with Manage Avatars permission in room R where Avatar Veil is enabled When the moderator toggles "Disable avatars" ON Then all participants’ generated avatars in room R are replaced with the neutral placeholder mask and all avatar status animations are suppressed within 2 seconds And users cannot see or select generated avatars while disabled, including in the roster, reactions, and check-in tiles And no personal identifiers are revealed; only pseudonyms remain visible And the disable state persists across app restarts and reconnects until toggled OFF by a moderator And an audit entry is recorded for the action When the moderator toggles "Disable avatars" OFF Then each participant’s last valid generated avatar is restored within 2 seconds
Per-user Per-room Avatar Override
Given a moderator with Manage Avatars permission in room R And a participant P currently has a generated avatar A1 When the moderator applies "Override avatar" to P in room R, selecting neutral mask NM-01 Then P’s avatar in room R is replaced by NM-01 within 2 seconds on all clients And P cannot change or reroll their avatar in room R while the override is active And the override affects only room R and does not change P’s avatar in other rooms And the action is reversible by moderators, restoring A1 if not rerolled since And an audit entry is recorded for apply and for remove
Abuse Report-triggered Avatar Reroll via Report Flow
Given an abuse report on participant P’s avatar in room R exists in the moderation queue When a moderator opens the report detail and selects "Reroll avatar" Then a new avatar A2 is generated using safe-listed palettes/shapes and a sanitized seed that ignores offensive custom names And A2’s identifier differs from the previous avatar A1 and A1 is retired for room R And the change propagates to all clients in room R within 2 seconds And the report is updated to Resolved with action type REROLL and linked audit entry And the reroll respects server-enforced rate limits for P in R; if exceeded, the moderator is shown a rateLimitExceeded error with retryAfter
Reroll Rate Limit Enforcement
Given participant P in room R has had 3 avatar rerolls in the last 24 hours When any client or moderator attempts another reroll for P in room R Then the server rejects the request and returns error code rateLimitExceeded with a positive retryAfter timestamp in ISO-8601 And no avatar image or seed is changed And an audit entry records the denied attempt without storing PII And when the 24-hour window elapses, the same request succeeds and produces a new avatar with a new identifier
Seed Generation Ignores Offensive Custom Names
Given a participant P has a custom display name that matches the offensive-name blocklist or ML toxicity threshold When P joins or updates their avatar in room R Then the seed input for avatar generation excludes the offensive name and uses a sanitized fallback (e.g., hashed pseudonym and roomId) And the resulting avatar uses only safe-listed palettes and shapes And the decision and filter reason are recorded in audit logs without storing the offensive name content And no client UI surfaces the offensive name in any avatar-related context
Safe-listed Palettes and Shapes Enforcement
Given the avatar generator is configured with a signed safelist manifest version V When any avatar is generated or rerolled Then only assets present in manifest V (palettes, shapes, animations) are eligible for selection And any attempt to load or select an asset not in the safelist is rejected server-side and replaced with a safe fallback And a daily integrity job verifies the safelist signature and reports deviations; generation halts if verification fails And all generated avatars pass the automated harmful-symbol detection check before being delivered to clients
PII-safe Audit Logging for Moderation Actions
Given moderation actions occur in room R (disable, override, reroll, rate-limit deny) When any such action is executed Then an immutable audit log entry is written containing: action type, roomId, pseudonym or participantId (non-PII identifier), acting moderatorId, timestamp, prior and new avatar identifiers or state, and reason code/reportId if applicable And the audit store forbids storage of email, IP, device identifiers, or legal names And access to audit entries is restricted to authorized roles and is queryable by roomId and timeframe And audit data is retained for 180 days and then purged, with purge events recorded

Time Blur

Control how precise your public timestamps appear—choose ranges like “within 15 minutes,” “morning,” or “in-window.” Stays compatible with rolling windows so accountability remains while hiding your exact routine. Protects privacy for shifting schedules, travel, and Do Not Disturb days.

Requirements

Blur Preset Library & Custom Windows
"As a privacy-conscious member, I want quick blur presets and a custom range option so that I can protect my routine while still signaling timely adherence."
Description

Provide built-in blur presets (±5 min, ±15 min, ±60 min; Morning 05:00–11:00; Afternoon 11:00–17:00; Evening 17:00–22:00; Night 22:00–05:00; In-Window tied to the habit’s rolling check-in window) plus a custom range picker with minimum width to prevent identifiability. Allow users to set defaults per account, per room, and per habit, and to override at check-in with a one-tap selector. Persist preferences in user and habit settings; expose via API so feed/profile/room views consistently render the same label and range. Enforce a safe default for new users and fallbacks when presets change. Benefits: fast selection, stronger privacy, flexibility for shifting schedules, while preserving accountability semantics.

Acceptance Criteria
Safe Default Blur for New Users
Given a new user with no blur preferences When they view their profile, feed, or any room, or perform their first check-in Then the system applies the safe default blur preset to their public timestamps And the displayed label matches the preset name across all views And exact check-in times are hidden from all viewers except the user And the safe default is stored as the account-level default until changed
Preset Library Availability and Labels
Given a user opens the blur selector When preset options are displayed Then the following presets are available with exact labels: ±5 min; ±15 min; ±60 min; Morning (05:00–11:00); Afternoon (11:00–17:00); Evening (17:00–22:00); Night (22:00–05:00); In-Window And selecting In-Window ties the blur range to the habit’s active rolling check-in window for that check-in And Night spans across the day boundary correctly (22:00 to next day 05:00) And all preset labels render in the user’s local time zone without altering stored UTC boundaries
Create Custom Blur Range with Minimum Width
Given MIN_CUSTOM_WIDTH is configured to 15 minutes And a user opens the custom range picker When the user selects start and end times Then the system enforces that end − start ≥ MIN_CUSTOM_WIDTH (supporting ranges that cross midnight) And if the selection violates the minimum, the confirm action is disabled and an inline validation message is shown And when a valid range is saved, the label displays as Custom (HH:MM–HH:MM) in the user’s local time
Set and Resolve Blur Defaults by Scope
Given a user sets an account-level default preset A And sets a room-level default preset B in Room R And sets a habit-level default preset C for Habit H in Room R When the user checks in to Habit H in Room R without override Then the applied blur preset is C And when the user checks in to another habit in Room R with no habit-level preset Then the applied blur preset is B And when the user checks in to a habit with no room-level preset Then the applied blur preset is A
One-Tap Blur Override During Check-In
Given a resolved default blur preset is selected for the current check-in When the user taps the blur selector in the check-in composer Then the preset list opens with the current default preselected And when the user selects a different preset or a valid custom range Then the override applies to this check-in only without additional confirmation steps And the user’s saved defaults remain unchanged And the chosen label and range are applied to the published check-in
Persisted Preferences and API Rendering Consistency
Given a user updates their account, room, or habit blur defaults When the preferences are saved Then the settings persist in user and habit settings storage And the public API for check-ins returns the effective blur type, label, and UTC range boundaries used for that check-in (including In-Window boundaries captured or computed consistently) And feed, profile, and room views render the same label and range for the same check-in And changes to defaults do not retroactively alter previously published check-ins
Fallback Behavior When Presets Are Modified or Removed
Given a preset previously selected by a user is removed or renamed by the system When a user’s settings or an existing check-in references a missing preset Then the system falls back to the safe default blur preset for display and future check-ins And a non-blocking notice informs the user the next time they open the blur selector And a non-PII diagnostic event is logged for analytics
Rolling Window–Aware Obfuscation Engine
"As a user, I want my public check-ins to show a time range instead of the exact minute so that my privacy is protected without breaking streak validation."
Description

Implement a server-side obfuscation service that converts exact check-in timestamps into deterministic display buckets based on the selected blur preset, time zone, and habit’s rolling window. Public surfaces (feeds, profiles, room timelines) receive only the obfuscated range and a canonical label (e.g., “within 15 minutes,” “morning,” “in-window”), while streak verification and decay logic continue to use exact timestamps. Handle cross-day boundaries and DST/time-zone shifts; guarantee that bucketization never reveals a narrower range than specified. Provide API fields (display_time_range, display_label) and ensure cache coherence across platforms. Benefits: privacy by design, compatibility with rolling windows, and consistent rendering.

Acceptance Criteria
Deterministic Bucketization by Preset, Time Zone, and Rolling Window
- Given a check-in timestamp, selected blur preset, user time zone, and habit rolling window, When obfuscation is requested repeatedly over 30 days, Then the returned display_time_range and display_label are identical for identical inputs. - Given different blur presets are selected (e.g., within 15 minutes, morning, in-window), When obfuscation is requested, Then display_label matches the selected preset’s canonical label and display_time_range conforms to that preset’s bucket definition. - Given the user’s time zone is changed, When the same check-in is re-fetched, Then display_time_range is recalculated in the new time zone while remaining deterministic for that input set.
Privacy Bound: Never Reveal a Narrower Range Than the Preset
- Given preset within 15 minutes and a check-in at any minute, When obfuscation is computed, Then display_time_range width is >= 15 minutes, contains the exact timestamp, and the boundaries align to the preset’s rounding rules. - Given preset morning defined as the local interval 05:00–12:00, When obfuscation is computed for any check-in in that period, Then display_time_range equals 05:00–12:00 local and is not reduced by the rolling window. - Given preset in-window and a rolling window spanning 23:45–00:30 local, When obfuscation is computed, Then display_time_range equals the full rolling window (cross-day allowed) and is not split or narrowed. - Given a DST transition day, When any preset bucket is computed, Then the display_time_range duration is never smaller than the preset’s minimum width and does not leak a narrower effective range.
Public Surfaces Receive Only Obfuscated Fields
- Given public feed, profile, and room timeline endpoints, When a check-in is retrieved by a public viewer, Then the payload contains display_time_range and display_label and does not contain exact timestamp fields (e.g., exact_check_in_at, created_at) for that check-in. - Given an internal streak verification process, When streak status is computed, Then exact timestamps are used for streak and decay logic, while public endpoints continue to omit exact timestamps. - Given API response validation, When schemas are inspected, Then no field or header reveals the exact check-in timestamp on public surfaces.
Cross-Day and DST Correctness
- Given a check-in at 23:58 local with preset within 15 minutes and a rolling window that crosses midnight, When obfuscation is computed, Then display_time_range may span two calendar dates and the label remains correct for the preset. - Given a DST fall-back transition where the hour repeats, When morning and in-window buckets overlap the repeated hour, Then display_time_range is returned with unambiguous ISO-8601 offsets and includes the exact instant, without narrowing below preset width. - Given a DST spring-forward transition where an hour is skipped, When computing any preset bucket, Then display_time_range uses valid local instants at the boundaries and still contains the exact timestamp in UTC terms.
API Schema and Canonical Output
- Given an obfuscation response, When inspected, Then display_time_range is an ISO-8601 interval string (start/end with timezone offsets) and display_label is a canonical label from the allowed set {"within 15 minutes","morning","in-window",...}. - Given a check-in exact timestamp T, When obfuscation is computed, Then T is within the interval [start, end) of display_time_range. - Given invalid or unsupported labels are attempted, When responses are returned, Then display_label is never outside the allowed set and defaults or errors are handled per API contract without exposing exact timestamps.
Cache Coherence and Invalidation Across Platforms
- Given a user changes their blur preset, When public surfaces are refreshed, Then iOS, Android, and Web display the updated display_time_range and display_label within 60 seconds. - Given a user changes their time zone, When public surfaces are refreshed, Then iOS, Android, and Web reflect the updated obfuscation within 60 seconds. - Given no change to inputs affecting obfuscation, When clients refetch within the cache TTL, Then responses are byte-identical (including ETag) across platforms, ensuring consistent rendering.
Audience-Specific Visibility Controls
"As a room participant, I want control over who sees my exact vs. blurred time so that I can stay accountable to my group without exposing my routine broadly."
Description

Add a policy layer that tailors timestamp precision by viewer context: self sees exact time; room members see the user’s chosen blur; moderators may request a narrower blur only with explicit member consent; public/external viewers always see the obfuscated range. Support per-room overrides, consent records, and clear UI affordances for what each audience will see. Enforce policies in API responses using viewer role and relationship to the check-in owner. Benefits: balances accountability and privacy, prevents accidental overexposure, and aligns with room governance.

Acceptance Criteria
Self Sees Exact Timestamp
Given a user has enabled Time Blur on their check-ins When the same user views their own check-in in the app or via API Then the exact timestamp (to the minute) is displayed in UI and returned by the API as exactTime and timestampPrecision=exact And no blur label is shown to the owner And the 'View as' preview allows the owner to toggle audiences (Self/Room Member/Moderator/Public) and reflects the correct visibility for each And analytics/export endpoints include the exact timestamp for the owner only
Room Members See Chosen Blur
Given a user posts a check-in with a selected blur setting (e.g., within_15m, within_1h, morning, in_window) And another user is a member of the same room without moderator privileges When the room member views the check-in via feed, profile, or notifications Then the timestamp is shown using the selected blur (e.g., 'within 15 minutes', 'morning') and not the exact minute And the API response omits exactTime and returns blurredTime and timestampPrecision matching the selected blur And the blurredTime falls within the rolling window for in_window settings And link previews and push notifications to room members use the blurred representation
Public/External Viewers Always See Obfuscated Range
Given a check-in is visible via a public profile, shared link, or embed And the viewer is not authenticated or not a member of the room When the viewer accesses the check-in Then only the obfuscated range is shown; exactTime is omitted from UI and API And all public endpoints (profile, share links, oEmbed) return timestampPrecision!=exact and include only blurred fields And caching/CDN responses do not contain exactTime for public traffic
Moderator Narrowing Request Requires Explicit Consent
Given a moderator requests a narrower blur for a member’s check-in(s) or timeframe When the member receives the request Then the UI shows a clear consent prompt with scope (which check-ins), audience (moderators), duration, and reason And the default is Decline; no change occurs until the member explicitly Accepts And upon Accept, only moderators see the narrowed precision; room members and public remain at the original blur And the system records consent with requesterId, memberId, scope, grantedBy, createdAt, expiresAt, and reason And upon Decline or no response by expiry, no narrowing is applied
Per-Room Overrides of Blur Policy
Given a user belongs to multiple rooms And the user sets a per-room timestamp blur override for Room A When the user posts check-ins that appear in Room A and Room B Then Room A uses the override setting while Room B uses the global user setting And the UI clearly indicates the active per-room policy on the compose and room settings screens And API responses for Room A reflect the override while responses for Room B do not
Consent Records Auditable and Expirable
Given a member has granted a consent to narrow blur for certain check-ins When a moderator or the member views the consent log Then the log lists entries with requesterId, scope, start/end times, current status (active/expired/revoked), and audit trail And consents automatically expire at the configured end time; after expiry, exact/narrowed views are no longer served And revoking a consent immediately removes narrowed access and is reflected in subsequent API responses
API Enforcement by Viewer Role and Relationship
Given an API client requests check-in details with an access token When the caller is the owner, a room member, a moderator, or a public/unauthenticated user Then the response includes timestamp fields according to policy: owner -> exactTime + timestampPrecision=exact; member -> blurredTime + timestampPrecision in {within_15m, within_1h, morning, in_window,...}; moderator -> as member unless a valid consent exists; public -> blurredTime only And attempts to request exactTime without authorization result in 403 with no exactTime in payload And policy is enforced consistently across feeds, profiles, notifications, search, and exports And each response includes a policyDecisionId for audit tracing
DND & Travel Smart Modes
"As a traveling creator, I want my timestamp blur to auto-adjust for DND and time-zone changes so that I don’t have to micromanage privacy while keeping my streak visible."
Description

Introduce automation rules that escalate blur precision during Do Not Disturb, focus sessions, or travel. When DND is active, default to a coarser preset (e.g., Daypart or In-Window). Detect time-zone changes and render daypart labels in the viewer’s local time while preserving streak logic in the user’s canonical habit zone. Provide a quick toggle and scheduled rules in Settings, with clear indicators on check-in cards. Benefits: reduces cognitive load on busy days, avoids inadvertent exposure while traveling, and keeps accountability intact.

Acceptance Criteria
Auto-Blur Escalation on DND/Focus Activation
Given Time Blur is enabled and a DND/Focus coarse preset is configured (default Daypart) When the OS DND or Focus session turns on Then the user’s blur preset auto-switches to the configured coarse preset within 5 seconds And any check-in made while DND/Focus is on renders using the coarse preset without minute-level precision And when DND/Focus turns off, the blur preset reverts to the last non-DND preset within 5 seconds And the active preset change persists across app restarts and device sleep And if OS DND/Focus access is unavailable, the app presents a one-time permission prompt and does not change presets until granted
Travel Time-Zone Change Detection and Blur Escalation
Given the device time-zone offset differs from the user’s canonical habit zone by ≥30 minutes When the app receives a system time-zone change event or is foregrounded Then Travel Smart Mode activates within 2 minutes And check-ins render using the Travel preset (default In-Window) regardless of prior fine-grain setting And a travel badge appears on the check-in card indicating Travel Smart Mode And streak evaluation continues against the canonical habit zone and rolling window rules And when the device returns within <30 minutes of the canonical zone, Travel Smart Mode deactivates and the prior blur preset is restored
Viewer-Localized Daypart Labels with Canonical Streak Preservation
Given a check-in exists under a coarse preset that uses dayparts When different viewers in different time zones view the same check-in Then the daypart label displays according to each viewer’s local time mapping And the streak status is computed using the check-in owner’s canonical habit time zone and rolling window And no viewer can access an exact timestamp beyond the selected blur preset And an info hint clarifies that daypart labels are shown in the viewer’s local time
Quick Toggle and Scheduled Rules Precedence
Given a Smart Modes quick toggle and scheduled rules are configured in Settings When the user toggles Smart Modes on from the main UI Then the selected Smart Mode applies immediately (≤2 seconds) and overrides scheduled rules until toggled off And precedence is Manual Toggle > Travel Smart Mode > OS DND/Focus > Scheduled Rules And overlapping scheduled rules resolve to the coarsest resulting preset And rule changes persist across sessions and sync across devices within 30 seconds And users can individually enable/disable Travel, DND/Focus, and Schedule categories
Smart Mode Indicators on Check-in Cards
Given a check-in is rendered under a Smart Mode (DND, Focus, Travel, or Schedule) When the check-in is displayed in feed, room, and profile history Then an icon and label indicate which Smart Mode applied, consistently across views And tapping the indicator opens an explainer showing the mode and active preset without exposing exact time And the indicator includes accessible labels and meets WCAG AA contrast And no indicator is shown when Smart Modes are inactive
Rolling Window Accountability Under Time Blur and Travel
Given a habit with a rolling window configuration When a check-in occurs under coarse blur due to DND/Focus or Travel Then on-time/late is computed strictly by the canonical habit zone and rolling window rules, independent of viewer location And DST shifts or midnight crossings do not incorrectly change the on-time result And crossing the International Date Line does not break the streak if the check-in is within the window And if the device time zone cannot be determined, the app falls back to the canonical zone, applies the configured coarse preset, shows a non-sensitive "Zone Unknown" indicator, and does not expose exact time
Consistent UI Labels & Accessibility Indicators
"As a mobile user, I want clear, consistent indicators when my time is blurred so that I understand exactly what others will see."
Description

Deliver cross-platform UI components that render blurred times with clear labels, icons, and tooltips (e.g., “Time blurred: Morning”). Update check-in composer, feed cards, room timelines, and profiles for consistent presentation and empty/loading states. Localize labels, provide ARIA/VoiceOver text announcing the blur level, and ensure contrast and touch targets meet accessibility guidelines. Include analytics hooks for preset usage and error telemetry for mismatched labels. Benefits: clarity, trust, and inclusive access across iOS, Android, and Web.

Acceptance Criteria
Cross-Surface Blur Label Consistency
Given a user has Time Blur enabled with a selected blur preset When the check-in is rendered in the check-in composer, feed cards, room timelines, and profiles Then each surface displays the same localized label text for the preset And the same blur icon is shown adjacent to the label And a tooltip/infotip is available on hover (Web) or tap (mobile) reading "Time blurred: {label}" And the exact timestamp is not visible on any surface when a blur preset is active And empty/loading states show a neutral placeholder for the blurred time without revealing exact time
Accessible Announcements for Blurred Time
Given a screen reader is active (VoiceOver, TalkBack, or ARIA-supported browsers) When focus moves to a blurred time element Then the assistive tech announces "Time blurred: {label}" And does not announce the exact timestamp And exposes the element with the correct role (button if it opens a tooltip, text otherwise) And keyboard activation (Enter/Space) or double-tap triggers the tooltip content announcement on all platforms And the focus order places the blurred time after the check-in author and before action buttons
Localization and RTL Support
Given the app language is set to any supported locale, including RTL languages When blurred time labels, icons, and tooltips are displayed Then labels are fully localized using locale-appropriate day-period terms And RTL locales mirror icon/label order and alignments And if a translation is missing, an English fallback is shown without placeholders And labels do not overflow or clip at 320px viewport width; overflow is ellipsized and the full text appears in the tooltip
Contrast and Touch Target Compliance
Given the blurred time label and icon are rendered on any supported theme (light, dark, high contrast) When measured against WCAG 2.1 AA Then text contrast ratio is at least 4.5:1 against its background And icon contrast ratio is at least 3:1 And interactive targets (label or info icon) meet minimum size 44x44 pt (iOS) and 48x48 dp (Android/Web) with adequate hit slop
Rolling Window Compatibility and Mismatch Telemetry
Given a check-in uses a rolling window blur preset (e.g., in-window or within 15 minutes) When the UI renders the blurred time Then the label reflects the active preset wording exactly And the exact timestamp is never displayed And if the preset from the backend does not match the visible label, an error event timeBlur.labelMismatch is logged with preset, label, platform, surface, locale, and build version And the UI falls back to a safe generic label "Time blurred" without crashing
Analytics Hooks for Preset Usage
Given a user selects or changes a blur preset in the composer When the selection is made and when a blurred time is rendered on any surface Then an analytics event timeBlur.presetUsed is emitted once per selection and at most once per surface render within 10 seconds And the payload includes preset, platform, surface, locale, userPrivacyState, and success=true/false And events are queued offline and retried up to 3 times on failure And no PII or exact timestamps are included in the payload
Privacy Resilience on DND and Time Zone Changes
Given the device enters Do Not Disturb or the user’s time zone changes When existing or new blurred check-ins are displayed Then only the blur label (e.g., Morning, within 15 minutes, in-window) is shown, never an exact time And labels map to the check-in’s intended day period based on the user’s current display locale and time zone at render time And tooltip text remains consistent with the visible label And any discrepancy triggers timeBlur.labelMismatch telemetry and shows the safe generic label
Anti-Gaming Safeguards & Audit Trail
"As a moderator, I want safeguards and logs around time blurring so that members can’t exploit it to fake adherence."
Description

Store immutable exact timestamps server-side with cryptographic hashing and maintain a versioned log of blur settings per check-in. Enforce rate limits and cooling periods on blur changes, apply a minimum blur width, and block toggling after posting to prevent misrepresentation. Provide moderator tools to review obfuscated evidence with member consent and backend queries to flag suspicious patterns. Benefits: preserves integrity of streaks and trust in accountability rooms while honoring privacy.

Acceptance Criteria
Immutable Exact Timestamp and Cryptographic Hashing
Given a user submits a check-in, When the server accepts the request, Then it persists the exact UTC timestamp and a cryptographic hash of {user_id, room_id, check_in_id, timestamp, blur_version} atomically. Given any subsequent request attempts to update the exact timestamp for that check-in, When processed, Then the API responds 409 Conflict and no data is mutated. Given an audit verification request with the original fields, When the hash is recomputed, Then it matches the stored hash exactly. Given database replication or restore, When the check-in is retrieved, Then the stored timestamp and hash remain unchanged.
Versioned Blur Settings Log Per Check-In
Given a check-in draft exists, When the user changes the blur option prior to posting, Then a new blur_version entry is appended with fields: version_id, created_at, actor_id, blur_type, blur_width, previous_version_id, and no prior versions are modified. Given a request to fetch the blur version history by the check-in owner or an authorized moderator with consent, When executed, Then the API returns an immutable, chronologically ordered list of versions. Given an attempt to delete or overwrite a blur_version entry, When processed, Then the API responds 405 Method Not Allowed or 409 Conflict and the log remains intact.
Blur Change Rate Limits and Cooling Period
Rule: A user may change blur settings at most 3 times per rolling 24 hours across all rooms; exceeding this returns 429 Too Many Requests and includes a Retry-After header. Rule: After any blur setting change, a 10-minute cooling period applies before the user can post a check-in in that room; attempts to post during cooling return 403 Forbidden with remaining_seconds. Rule: Rate limit and cooling counters are enforced server-side and reflected via a limits endpoint including remaining_changes and cooldown_remaining_seconds.
Minimum Blur Width Enforcement
Rule: The system enforces a minimum blur width of 15 minutes for any time-range based blur; values below 15 minutes are rejected with 400 Bad Request and error_code 'BLUR_WIDTH_MIN_VIOLATION'. Rule: The response for accepted blur settings includes effective_blur_width_minutes >= 15. Rule: Predefined buckets (e.g., Morning/Afternoon/Evening) are mapped to widths >= 180 minutes and may not be narrowed below the minimum.
No Post-Publication Blur Toggling
Given a check-in is posted, When the user attempts to change its blur setting, Then the API responds 409 Conflict and no new blur_version is created. Given a check-in is posted, When the public payload is requested, Then the blur value remains the one present at post time. Given an unauthorized client attempts to mutate via direct API, When processed, Then the action is blocked and a security audit event 'BLUR_TOGGLE_BLOCKED' is recorded.
Moderator Review With Member Consent
Given a member grants explicit consent scoped to specific check-ins with an expiry timestamp, When a moderator with role 'moderator' requests review, Then the API returns exact timestamps and the blur version log for the scoped check-ins only. Given consent is expired or revoked, When a moderator requests review, Then the API returns 403 Forbidden and no sensitive data is returned. Given a moderator accesses consented data, When the request completes, Then an access log entry is written including moderator_id, member_id, scope, and timestamp, and is retrievable by the member.
Suspicious Pattern Detection and Flagging
Rule: The system flags a user with code 'SUSPECT_BLUR_BEHAVIOR' when any of the following occur within a 14-day window: (a) 4+ check-ins where blur was changed within 10 minutes prior to posting; (b) 5+ check-ins posted within the final 60 seconds of the allowed window; (c) >2 rate-limit violations for blur changes. Rule: When a flag is generated, an event record is stored with user_id, rule_triggered, sample_check_in_ids, and created_at; the record is queryable via a moderator API. Rule: Flags do not alter streaks or visibility and are visible only to moderators.
Defaults, Onboarding, and Experimentation
"As a new user, I want sensible default time blurring and a brief explanation so that I can start safely without configuring everything upfront."
Description

Set safe default blur (e.g., ±15 minutes) for new users and habits, with a guided tooltip in first check-in and a Settings education card explaining presets and trade-offs. Enable remote configuration and A/B tests for default levels and copy, and instrument metrics for adoption, retention, and report rate. Provide migration scripts to backfill display ranges for historical check-ins where applicable. Benefits: smooth adoption, measurable impact, and the ability to tune defaults without app releases.

Acceptance Criteria
Default Blur Applied on New Account and First Habit
Given a new user with no prior habits And remote config has an active default blur preset (preset_id) When the user creates their first habit Then the habit's blur setting is initialized to the active default preset And the user's global blur default is set to the same preset And the first public check-in displays the timestamp using that blur And the display remains compatible with rolling windows, showing in-window compliance without exposing exact time And an analytics event "time_blur_default_applied" is emitted with user_id, habit_id, preset_id, experiment_variant, and timestamp
First Check-in Tooltip Education
Given a user publishing their first public check-in When the check-in confirmation screen is shown Then a tooltip explaining Time Blur appears with primary action "Adjust Blur" and secondary action "Got it" And selecting "Adjust Blur" opens the blur settings for the current habit And selecting "Got it" dismisses the tooltip And the tooltip does not reappear for this user after either action And an analytics event "time_blur_tooltip_shown" is emitted once per user, and "time_blur_tooltip_action" with action_type is emitted upon interaction
Settings Education Card Explains Presets and Trade-offs
Given a user opens Settings When the Time Blur education card is visible Then the card lists available presets including ±15m, within-morning, and in-window And each preset shows a short description of privacy vs accountability trade-offs And tapping "Learn more" opens a details view with examples compatible with rolling windows And changing the default preset updates the user-level default and does not modify past check-ins And analytics events "time_blur_settings_card_viewed" and "time_blur_default_changed" are emitted with preset_id and previous_preset_id
Remote Configuration Overrides Defaults and Copy Without App Release
Given remote configuration is updated with new default preset and tooltip/education copy text When clients fetch remote config Then the new default preset is applied to new users and newly created habits only And existing user/habit blur settings are not modified And the updated copy appears in the tooltip and education card within 1 hour of publish And a kill switch flag disables Time Blur onboarding surfaces within 15 minutes when toggled And all values are logged via "remote_config_applied" with config_version and client_app_version
A/B Test for Default Levels and Copy
Given an experiment with variants Control, PresetA, and PresetB is live And eligibility excludes users who have manually changed blur before assignment When an eligible user starts the app session Then the user is randomly and deterministically assigned to a variant and remains sticky for 90 days And the assigned variant determines the default blur preset and/or copy shown And sample ratio mismatch is monitored and alerts if any variant deviates by >3 percentage points after 10k assignments And experiment metadata is attached to all relevant analytics events
Metrics for Adoption, Retention, and Report Rate
Given analytics is operational When users interact with Time Blur onboarding and settings Then the following events are captured with required properties: time_blur_default_applied, time_blur_tooltip_shown, time_blur_tooltip_action, time_blur_settings_card_viewed, time_blur_default_changed, check_in_published_with_blur, report_privacy_issue_submitted And each event includes user_id, habit_id (if applicable), preset_id, experiment_variant, and timestamp And at least 95% of public check-ins have associated blur metadata within 24 hours And weekly dashboards compute adoption rate (kept default vs changed), 7-day retention by variant, and report rate per 1k active users
Migration Backfill for Historical Check-ins
Given historical public check-ins lacking blur display ranges When the migration script runs in production with config preset_id Then it backfills display ranges based on the selected preset without altering stored exact timestamps And it skips records already backfilled and private check-ins And it completes 1 million record backfill in under 2 hours with progress logging every 10k records And it records audit logs of total processed, updated, skipped, and errors And a rollback script can restore previous display values using the audit log And post-migration, public feeds show blurred ranges for affected check-ins

Ghost Cred

Earn trust without identity via privacy-preserving signals: current streak, on-time rate, host-verified sessions, and reaction reliability. Hosts can set minimum Ghost Cred to join or speak, keeping rooms high-quality while allowing anonymous participation. Motivates consistent check-ins without exposure.

Requirements

Ghost Cred Scoring Engine
"As an anonymous participant, I want my consistency and reliability to be recognized without revealing my identity so that I can access quality rooms while staying private."
Description

Implement a backend scoring engine that computes a normalized 0–100 Ghost Cred score using privacy-preserving aggregates of four signals: current streak length, on-time check-in rate, host-verified session ratio, and reaction reliability. Apply configurable weights and time decay to reward recent consistency while preventing sudden spikes from gaming. Produce both a score and a discrete tier used by gating (e.g., Starter, Steady, Trusted). Expose a low-latency API (<100 ms p95) for room joins, speaker requests, and roster sorting. Cache scores locally with short-lived validity to support offline-ready check-ins and reduce server load. Log versioned inputs and weights for auditability, with feature flags to tune weights without app updates. Integrate with existing habit and session models so scoring updates reflect real-time check-ins and verifications.

Acceptance Criteria
Normalized Weighted Score With Time Decay And Anti-Gaming
Given privacy-preserving aggregates S=current streak, O=on-time rate, V=host-verified ratio, R=reaction reliability and configured weights W (sum=1.0) with decay config D (versioned) When the scoring engine computes Ghost Cred Then the score is in [0,100] and is deterministic for the same (S,O,V,R,W,D) And only aggregate inputs (S,O,V,R) are used; no raw event-level data is accessed And time decay D is applied so that older events contribute less than newer ones And partial derivatives are non-negative: increasing any one of S,O,V,R (others fixed) does not decrease the score And if maxDeltaPerWindow is configured (e.g., 10 points per 24h), the absolute score change over that window does not exceed the configured bound And the computation returns a version tag combining the scoring code version and the (W,D) config versions
Discrete Tier Mapping For Gating
Given a tier configuration T (e.g., Starter: 0–39, Steady: 40–74, Trusted: 75–100) with inclusive boundaries and version tag When a score is produced Then the response includes the correct tier label per T and tierVersion And boundary scores map deterministically to the higher tier at the lower bound (e.g., 40 -> Steady) When a host sets a minimum required tier for join/speak Then join/speak is permitted only if userTier >= requiredTier and denied otherwise with a machine-readable reason code And roster sorting uses score (desc) with a stable sort for equal scores
Low-Latency Score API For Room Entry And Speaker Requests
Given the Score API is deployed in production with observability enabled When measured over rolling 5-minute windows under p50 expected traffic Then p95 latency < 100 ms and p99 latency < 200 ms, success rate >= 99.9% And responses include {score, tier, scoreVersion, weightsVersion, decayVersion, tierVersion, expiresAt, cached:boolean} And responses include no PII and no raw event data; only aggregates-derived outputs When upstream scoring datastore is slow or unavailable Then the API serves the most recent score within its validity window (expiresAt) with cached=true And if no valid cached score exists, the API returns a retriable error with Retry-After and no score body
Local Score Cache TTL And Invalidation Rules
Given a configurable TTL (e.g., 10 minutes) for score validity provided via expiresAt When a client has a cached score that is not expired Then subsequent joins/speak requests may use the cached score without re-fetching When the user performs a check-in or receives a host verification Then the client invalidates the cached score immediately and attempts to refresh When offline and the cached score is not expired Then the client allows offline check-in and queues a score refresh for the next connectivity window And upon reconnection the server recomputes and the client replaces the cached value with the new score and tier
Versioned Audit Logging And Reproducibility
Given any score computation request When the computation completes Then an audit log entry is written with {pseudonymousUserId, timestamp, score, tier, aggregates:{S,O,V,R}, weightsVersion, decayVersion, tierVersion, scoreCodeVersion, featureFlagSnapshotId, requestId} And no PII (e.g., username, email, IP) is persisted in the audit payload When auditing replays the entry using the recorded versions and aggregates Then the recomputed score equals the logged score within tolerance ±0.1 And audit entries are queryable by requestId and by version tuple for investigation
Feature-Flagged Weight Updates Without App Releases
Given new weights/decay configs are rolled out via feature flags with staged percentages (0%→25%→100%) When a stage change is applied Then eligible clients/app servers adopt the new config within 60 seconds and include the new versions in responses And no mobile/desktop app update is required And p95 API latency remains < 100 ms during and after rollout When the flag provider is unavailable Then the engine continues with the last-known-good config and logs the incident without interrupting scoring
Real-Time Updates From Habit And Session Models
Given a user check-in or host verification event is committed in the habit/session models When the event is processed Then the user’s Ghost Cred is recomputed and published such that the API reflects the new score within 3 seconds p95 And room roster sorting reflects the updated score within 5 seconds p95 When an event is retracted or corrected Then the score is recomputed to reflect the latest truth and prior incorrect computations are superseded And concurrent events do not produce conflicting scores (idempotent, last-write-wins with versioning)
Host Threshold Controls & Enforcement
"As a host, I want to set minimum Ghost Cred levels for joining and speaking so that my room stays high-quality without requiring participants to reveal their identity."
Description

Provide host-facing controls to set minimum Ghost Cred thresholds for joining a room, requesting mic, and co-hosting. Include presets (Open, Balanced, High-Trust) and custom sliders, with inline impact previews estimating allowed/blocked percentages based on current audience. Enforce gating server-side during join/speak flows with clear client messaging and a one-time appeal request option that does not reveal identity. Support per-room overrides, scheduled changes, and temporary relaxations for events. Record gating decisions for analytics and abuse review. Expose an admin dashboard widget to monitor threshold effects (join attempts blocked, conversion after coaching) and to roll back changes quickly.

Acceptance Criteria
Preset and Custom Threshold Configuration with Impact Preview
Given a live room with at least 100 current audience members whose Ghost Cred attributes are available When the host selects the preset "Balanced" Then the join, mic, and co-host sliders populate with the Balanced default values defined for this preset And the inline impact preview displays allowed% and blocked% computed against the current audience distribution And for a seeded test dataset, the preview percentages match the backend reference calculation within ±1 percentage point When the host switches to "Custom" and adjusts any slider Then the inline preview updates within 300 ms after the last input And saving applies the configuration to the room and persists it to durable storage When the current audience size is fewer than 10 Then the preview shows a "Low data" state and a fallback estimate based on the last 7 days for this room
Server-Side Enforcement for Room Join Gating
Given room threshold settings are active And a user’s Ghost Cred is below the room’s join minimum When the user calls POST /rooms/{room_id}/join Then the server responds 403 GATE_BLOCKED with reason_code=GHOST_CRED_BELOW_MIN and includes required_thresholds, action=join, and a non-identifying user_cred_snapshot And the response payload excludes email, phone, legal name, and device identifiers And the request remains blocked if client headers, query params, or feature flags are modified And a decision log entry is written with decision=blocked Given a user whose Ghost Cred meets or exceeds the join minimum When the user calls POST /rooms/{room_id}/join Then the server responds 200 JOINED with room state And a decision log entry is written with decision=allowed
Server-Side Enforcement for Mic and Co-Host Requests with Client Messaging
Given room threshold settings for mic and co-host are active And a user is below the mic threshold When the user calls POST /rooms/{room_id}/mic-request Then the server responds 403 GATE_BLOCKED with reason_code=MIC_THRESHOLD And the client displays a standardized message explaining the mic requirement and shows a visible "Request one-time appeal" CTA And a decision log entry is written with action=mic_request Given a user is below the co-host threshold When the user calls POST /rooms/{room_id}/cohost-request Then the server responds 403 GATE_BLOCKED with reason_code=COHOST_THRESHOLD And the client displays a standardized message explaining the co-host requirement and shows a visible "Request one-time appeal" CTA And a decision log entry is written with action=cohost_request Given a user meets each respective threshold When they perform the same requests Then the server responds 200 and the actions proceed
One-Time Anonymous Appeal Flow
Given a blocked user who has not used an appeal in this room for the same action When the user taps "Request one-time appeal" Then the server creates a room- and action-scoped appeal token, non-identifying and single-use, and returns status=pending_approval And no email, phone, legal name, or device identifiers are stored or transmitted in the appeal payload When the host approves the appeal Then the user can complete the blocked action once within 15 minutes of approval And after successful use, the appeal token is invalidated and cannot be reused When the user attempts a second appeal for the same room and action Then the server responds 409 APPEAL_ALREADY_USED And all appeal events are logged with decision, timestamp, and non-identifying user reference
Per-Room Overrides, Scheduled Changes, and Temporary Relaxations
Given a default threshold policy exists When the host enables a per-room override and saves Then the override becomes the effective policy for that room immediately and is persisted When the host schedules a threshold change for a future timestamp T (room timezone) Then the new thresholds become effective at T±1 minute and an audit record is created with scheduler identity and prior values When the host configures a temporary relaxation window [A,B] Then during [A,B] the effective thresholds use the specified relaxation values And after B the prior thresholds auto-restore without manual action And no forced ejection events are emitted for users already in the room; gating applies only to new join/mic/co-host attempts
Comprehensive Gating Decision Logging for Analytics and Abuse Review
Given gating decisions occur for join, mic, or co-host When any decision is made (allowed or blocked) Then a log record is written containing: timestamp, room_id, action, effective_thresholds, user_ghost_cred_snapshot (non-identifying), decision, reason_code, appeal_token_used (boolean), and server_version And the record is queryable in the analytics store within 60 seconds for 95% of events And log records are append-only; updates are prohibited, and deletions are allowed only via admin GDPR purge with an audit trail
Admin Dashboard Monitoring and Rapid Rollback
Given an admin opens the threshold effects widget When a room and time range are selected Then the widget displays metrics: total attempts, blocked count, block rate, appeal requests, appeal approvals, and 24h post-coaching conversion, segmented by action (join/mic/co-host) And metric freshness (ingest-to-UI latency) is under 5 minutes for 95% of updates When the admin clicks "Rollback thresholds" and confirms Then the room reverts to the immediately previous configuration within 2 minutes And server-side enforcement reflects the rollback And an audit entry captures who performed the rollback and why
Privacy-Preserving Signal Proofs
"As a privacy-conscious user, I want my participation signals to remain anonymous and minimally shared so that I can build trust without exposing my identity or detailed behavior."
Description

Store raw behavioral signals on-device and transmit only signed attestations (non-PII aggregates) required for Ghost Cred computation. Use opaque, rotating pseudonymous identifiers per user and per room to minimize cross-room linkability, while maintaining a stable trust lineage for abuse mitigation. Apply server-side aggregation that discards precise timestamps after scoring and retains only minimal evidence needed for audits. Redact or bucket metrics shown to hosts (e.g., tiers instead of exact counts). Provide a privacy mode that adds calibrated noise to displayed aggregates without affecting internal scoring. Ensure data retention limits, encryption at rest and in transit, and documented APIs specifying what data is collected, processed, and exposed.

Acceptance Criteria
Device-Signed Attestations Only
Given a user performs check-ins, reactions, or joins a session When the client prepares data for transmission Then only signed attestations containing non-PII aggregates are sent (room pseudonymous ID, streak length, on-time rate bucket, reaction reliability score, host-verified session count, attestation window, signature) and no raw event logs or precise timestamps are included And the server validates signatures and attestation freshness within the configured window and rejects invalid, tampered, or stale attestations with an explicit error And network inspection across 100 simulated events shows zero instances of raw behavioral signals or PII leaving the device
Per-Room Rotating Pseudonymous Identifiers
Given a user participates in multiple rooms When the user joins any room Then the client uses a unique opaque pseudonymous ID per room that does not match IDs used in other rooms And the pseudonymous ID rotates per room on the configured rotation schedule without disrupting the user’s Ghost Cred continuity in that room And cross-room correlation tests confirm <1% false linkability at p<0.05 using traffic and payload analysis And a server-held trust lineage token exists for abuse mitigation but is never exposed via client APIs or host views
Server Aggregation With Timestamp Discarding
Given the server receives valid attestations When Ghost Cred scoring is computed Then precise event timestamps are not persisted post-scoring; only windowed buckets, score, attestation hash, signature, room pseudonymous ID, and expiration are retained And any precise timestamps present in inbound attestations are discarded within the same processing transaction And database schema and logs evidence no storage of precise timestamps and retention of minimal audit evidence only And retained audit artifacts are automatically purged at or before the configured retention limit
Host-Facing Metrics Redacted to Buckets
Given a host views a participant’s Ghost Cred in a room When metrics are displayed or used for gating Then only bucketed/tiers are shown (e.g., streak tier, on-time rate bucket, reaction reliability tier, host-verified session tier) with no exact counts or per-event details And host-available APIs return only bucket labels and tier identifiers; exact numeric values are excluded And join/speak gates accept configurable thresholds expressed in tiers/buckets and never require or expose exact raw values And UI/API snapshots confirm no leakage of precise counts or timestamps
Privacy Mode With Calibrated Noise (Display-Only)
Given a user enables Privacy Mode When aggregates are rendered to other participants or the host Then calibrated noise is applied to displayed aggregates within configured bounds without changing internal scoring or gating outcomes And for the same underlying data, access decisions (join/speak) are identical with Privacy Mode on vs off And displayed values deviate by no more than one bucket from the underlying bucket with probability ≥95% over 1,000 trials And API responses are flagged as privacy-noised and never include seeds or parameters that enable de-noising
Encryption and Data Retention Enforcement
Given client-server communication occurs When data is transmitted Then TLS 1.2+ is enforced with modern ciphers and all endpoints refuse plaintext And at rest, server-side artifacts (scores, audit hashes, signatures) are encrypted with managed keys and documented rotation And retention policies automatically purge audit artifacts and scores per configured limits, with observable deletion logs and metrics And attempted retrieval of expired artifacts returns explicit expiry errors and no data
Documented APIs and Data Transparency Map
Given developers and hosts integrate with StreakShare When accessing API documentation Then the public/internal docs enumerate all collected, processed, and exposed fields for Ghost Cred, including purposes, lawful bases (if applicable), retention periods, and visibility by actor (user, host, server) And the docs explicitly state that raw behavioral signals remain on-device and only signed attestations are transmitted And an automated doc linter validates that every exposed field in the API schema has a documented purpose and retention rule And API responses include versioned schema metadata that matches the published documentation
Host-Verified Session Workflow
"As a host, I want an easy way to verify attendee participation so that trustworthy users can earn higher Ghost Cred without manual overhead."
Description

Enable hosts to verify sessions with one tap (Verify Session) that confirms presence and timeliness of attendees’ check-ins for that time block. Support automatic verification windows tied to scheduled room times, with grace periods for late arrivals and separate handling of re-joins. Allow co-host delegation and rate-limited bulk verification to prevent abuse. Show hosts a compact queue of recent check-ins awaiting verification and allow quick dismissals for suspected spam. Feed verification outcomes into the scoring engine in near real time, and log verification actions for transparency and dispute resolution.

Acceptance Criteria
One-Tap Session Verification by Host
Given an active room within a scheduled time block and pending attendee check-ins eligible for verification When the host taps "Verify Session" on a specific attendee Then the system marks that check-in as Host-Verified with fields: verification_id, attendee_pseudonymous_id, host_id, room_id, time_block_id, verified_at (UTC), verification_source="manual_tap" Given a successful verification When the host returns to the queue Then the verified item is removed and the pending count updates within 1 second Given network latency within normal bounds When verification succeeds server-side Then the UI surfaces a "Verified" confirmation state within 500 ms p95 after server acknowledgement Given a transient failure (e.g., timeout) When the host retries using an idempotency request_id Then exactly one verification is persisted and the UI presents an actionable retry within 2 seconds p95
Automatic Verification Window with Grace Period
Given a scheduled time block with start_at and end_at configured for a room When a check-in occurs within [start_at, end_at + grace_minutes] Then it is eligible for host verification; default grace_minutes=5, configurable 0–15 at the room level Given a check-in before start_at or after end_at + grace_minutes When the host opens the verification queue Then the check-in does not appear as eligible for verification Given an eligible check-in timestamp When it is <= start_at + on_time_window_minutes (default 2, configurable 1–10) Then the item is labeled "On-Time"; otherwise it is labeled "Late" but remains eligible within the grace window Given room-level configuration changes to grace or on_time_window When saved Then they apply to subsequent check-ins and do not retroactively change prior labels or eligibility
Handling Late Arrivals and Re-joins
Given an attendee checks in, disconnects, and re-joins within the same scheduled block When the host views the verification queue Then only the earliest eligible check-in appears as a single verification candidate, and a "Re-joined" badge with count is shown on the item Given an attendee has already been host-verified within the current block When they re-join again within the same block Then no additional verification action is allowed for that attendee in that block and the UI indicates "Already verified" Given an attendee re-joins after end_at + grace_minutes with no prior eligible check-in When the host views the queue Then no verification candidate is created for that attendee
Co-Host Delegation for Verification
Given a room owner assigns a user as co-host When the co-host opens the verification queue Then they can verify and dismiss items with the same permissions as the host, and all actions are attributed to the co-host in the audit log Given a co-host is removed by the owner When the former co-host attempts to verify or dismiss Then the API returns 403 and the UI prevents the action within 1 second Given multiple hosts/co-hosts act concurrently on the same check-in When two actors attempt to verify it Then exactly one verification succeeds and the other sees "Already verified" within 1 second
Rate-Limited Bulk Verification to Prevent Abuse
Given the verification queue contains multiple eligible items When the host selects bulk verify Then the system processes up to 10 check-ins per bulk action and enforces a limit of 60 verifications per minute per room per actor; exceeding limits returns a rate-limit error with retry_after seconds Given a bulk verify request with N selected items When processed Then the operation is atomic per request_id: either all N are verified or none, with clear success/failure feedback Given repeated bulk verification attempts When rate limits are enforced Then server-side enforcement latency is <200 ms p95 and no duplicate verifications are recorded
Compact Queue and Spam Dismissal
Given pending eligible check-ins exist When the host opens the queue Then a compact list shows items from the current block and the previous 10 minutes, sorted by check-in time ascending, including attendee pseudonymous handle, on-time/late tag, and re-join badge if applicable Given the host suspects spam When the host taps "Dismiss as spam" on an item Then the item is removed from the queue within 1 second, a dismissal event is logged with reason="host_spam_dismissal", and no scoring update is sent for that check-in Given a dismissal occurs When the host taps Undo within 5 seconds Then the item is restored to the queue and the dismissal log is marked reversed Given three or more dismissals from the same actor occur within 60 seconds for the same room When the next dismissal is attempted Then a confirmation step is required or the action is rate-limited to prevent accidental mass dismissals
Real-time Scoring Feed and Audit Log
Given a verification or dismissal completes When the event is emitted to the scoring engine Then the engine receives and processes it within 3 seconds p95 and updates Ghost Cred metrics accordingly (e.g., host_verified_sessions++, on-time rate recalculated when applicable) Given unreliable network conditions When events are retried Then idempotency keys ensure no duplicate scoring effects are applied Given any verification or dismissal action When logged Then an immutable audit log entry is created with fields: action, actor_id, attendee_pseudonymous_id, room_id, time_block_id, action_at (UTC), source, reason (optional), request_id, is_reversed (boolean), retrievable via API and in-product view with pagination and time filters Given privacy requirements When hosts review logs in-product Then no PII (e.g., email, phone, IP) is shown in the UI; IPs are stored server-side for abuse analysis only
Reaction Reliability Metric
"As a contributor, I want my meaningful reactions to increase my credibility so that I can participate more fully without having to expose my identity."
Description

Define and implement a reaction reliability signal that measures how consistently a user’s reactions align with session context without spam. Factors include timing relative to session events, diversity over time, avoidance of burst spamming, and host feedback (e.g., marking helpful reactions). Detect anomalous patterns (automated or coordinated reactions) and downweight them. Normalize the metric across rooms and cohorts to prevent room-type bias. Integrate the resulting reliability score into Ghost Cred with tunable weight, and expose rate limits and cooldowns to prevent farming. Provide internal dashboards to monitor false-positive/negative rates and adjust heuristics.

Acceptance Criteria
Composite Reliability Subscores Computation
1) Given a session with timestamped events and stored reactions, When the reliability score is computed, Then the system calculates four subscores: timing, diversity, host_feedback, and anti_spam, each in [0,1], and combines them into R_raw = 0.35*timing + 0.25*diversity + 0.25*host_feedback + 0.15*anti_spam. 2) Given reactions within ±5 seconds of a session event (start/checkpoint/wrap-up), When computing the timing subscore, Then those reactions receive timing weight 1.0; reactions >30 seconds from any event receive ≤0.3; reactions posted >120 seconds after wrap-up receive 0. 3) Given a 14-day rolling window and a reaction vocabulary of 6 types, When computing the diversity subscore, Then apply add-one smoothing and assign ≥0.9 if ≥3 types used and no type >60% of usage; assign ≤0.3 if only 1 type used with ≥20 reactions. 4) Given host_feedback and anti_spam subscores are empty due to no signals, When computing R_raw, Then missing subscores are imputed to 0.5 and the score is flagged with reason="low_signal". 5) Given identical inputs, When recomputing R_raw, Then the result is deterministic (bitwise-equal floating value to 4 decimal places).
Burst Spam Detection and Cooldown Enforcement
1) Given a user posts >4 reactions within any 10-second sliding window, When computing reliability, Then reactions beyond the 4th in that window are assigned weight 0 and a 30-second cooldown is applied. 2) Given three burst violations within a 24-hour period, When computing the user’s reliability, Then the anti_spam subscore is reduced by ≥0.2 for the next 24 hours. 3) Given a user remains at or below 4 reactions per 10 seconds for 30 consecutive minutes, When evaluating penalties, Then any burst penalty and cooldown are cleared automatically. 4) Given a reaction is blocked due to cooldown, When the client attempts to send it, Then the API returns HTTP 429 with Retry-After seconds and reason="cooldown" and the SDK surfaces a visible disabled state until cooldown elapses.
Host Feedback Integration
1) Given a host marks a reaction as Helpful within 2 minutes of its timestamp, When computing host_feedback subscore, Then that reaction’s weight increases by +0.2 up to a maximum of 1.0 and is included in the subscore aggregation. 2) Given a host marks a reaction as Unhelpful, When computing host_feedback subscore, Then that reaction’s weight becomes 0 and increments an unhelpful_count for the user. 3) Given a user accrues ≥3 Unhelpful marks from ≥2 distinct hosts within 7 days, When computing reliability, Then the host_feedback subscore is capped at ≤0.3 for the next 7 days. 4) Given any host feedback action, When retrieving audit logs, Then entries include reaction_id, host_id, action, timestamp, and effect, and are queryable within 60 seconds.
Anomalous/Automated Pattern Detection and Downweighting
1) Given two or more users produce identical reaction sequences (same types and timestamps within ±1 second) of length ≥5 within a 5-minute window in the same room, When evaluating reactions, Then those reactions are flagged as coordinated and downweighted by 80% in all subscores. 2) Given a single user’s inter-reaction intervals show coefficient of variation <0.05 over ≥20 reactions in a session, When evaluating automation risk, Then the session-level reliability contribution is capped at 0.2 and the user is placed on a 60-second cooldown after each reaction for the remainder of the session. 3) Given a host issues an override to mark flagged reactions as legitimate, When processing the override, Then the flags are cleared and original weights restored within 60 seconds and recorded in audit logs.
Cross-Room and Cohort Normalization
1) Given per-room distributions over the last 30 days with ≥200 unique users, When normalizing R_raw, Then convert each subscore to a room-level z-score and map to a global 0–100 scale using weekly updated percentiles to produce R_norm. 2) Given identical user behavior replayed across a co-working room and a study room, When comparing R_norm, Then the absolute difference is ≤10 points. 3) Given a room lacks sufficient data (<200 users), When normalizing, Then use cohort fallback based on room_size_band, session_length_band, and time_of_day, and include normalization_confidence in the output. 4) Given a normalization model update, When backfilling, Then 95% of historical reliability scores are recomputed within 24 hours.
Ghost Cred Integration With Tunable Weight
1) Given a normalized reliability score R_norm in [0,100] and a tunable weight w in [0.1, 0.5] (default 0.3), When computing Ghost Cred G, Then adjusting w by +0.1 changes G by the expected delta and the change is reflected in API/SDK responses within 60 seconds. 2) Given a host sets a minimum Ghost Cred threshold to join or speak, When users attempt to join/speak, Then users with G below the threshold are blocked and shown an explicit reason including reliability_score and reliability_weight. 3) Given API v1, When fetching a user’s Ghost Cred, Then the payload includes fields reliability_score (0–100), reliability_weight, and reliability_rate_limited (boolean).
Internal Dashboard: FP/FN Monitoring and Heuristic Tuning
1) Given a labeled evaluation set of reactions, When viewing the dashboard, Then precision, recall, and FP/FN rates for anomaly flags are displayed by day, room type, and cohort with filters and export to CSV. 2) Given a heuristic parameter change (e.g., burst_max_per_10s), When saving, Then the change is audited (who/what/when), deployed to a 10% canary within 5 minutes, and auto-rolled back if FP rate increases by >5% absolute versus baseline. 3) Given a session drill-down, When inspecting reactions, Then per-reaction features (timing offset, diversity contribution, host marks, burst window) and decision path are visible with latency <2 seconds for queries under 10k reactions.
Ghost Cred Transparency & Coaching
"As an anonymous participant, I want to see how my Ghost Cred is determined and how to improve it so that I can reach higher-trust rooms without revealing my identity."
Description

Provide a private, user-only view that explains current Ghost Cred tier and key drivers (e.g., on-time rate, verified sessions) with actionable tips to improve. Show trend over time and upcoming opportunities (next scheduled session, on-time reminder). For hosts, show only what’s needed: the participant’s tier and eligibility state, never raw history. Include lightweight dispute flow to report incorrect verifications or penalties, with asynchronous resolution and minimal data exposure. Ensure all messaging is non-identifying and consistent across platforms, and localize copy for clarity and motivation.

Acceptance Criteria
Private Ghost Cred Dashboard (User-Only Access)
Given an authenticated user opens their Ghost Cred dashboard, When the dashboard loads, Then the user sees their current tier label, on-time rate (% over the last 30 days), count of host-verified sessions (30-day and lifetime), reaction reliability score, and a last-updated timestamp. Given a user attempts to access another user's Ghost Cred dashboard via UI or API, When the request is made, Then the system denies access to detailed metrics and returns HTTP 403 for API calls and no detailed view in UI. Given backend reference values for the user exist, When the dashboard renders, Then all displayed metrics match backend reference values within 0.5% tolerance and use identical rounding rules. Given the dashboard is displayed, When inspecting the UI, Then no personally identifying information (name, email, phone, avatar image) is shown anywhere on the Ghost Cred surfaces.
Host View Minimal Data: Tier and Eligibility Only
Given a host views a participant in a room, When opening the participant card or roster details, Then only Ghost Cred tier, eligibility state (Eligible/Ineligible), and machine-readable reason codes are visible; no history, timestamps, rates, counts, or raw events are displayed. Given the client requests participant Ghost Cred via API, When the host is the requester, Then the payload includes only fields: {tier, eligibilityFlag, reasonCodes, version}; fields such as onTimeRate, verifiedSessions, reactionReliability, streakLength, timestamps are absent. Given a host attempts to export participant data, When the export is generated, Then Ghost Cred content is limited to tier and eligibility state per participant with no additional metrics. Given role-based access control is configured, When a non-host requests another user's Ghost Cred, Then no Ghost Cred data is returned (HTTP 403) beyond what is publicly allowed (none).
Trend and Driver Breakdown for Users
Given a user has 7 or more days of activity, When viewing Ghost Cred trends, Then a 30-day chart displays tier changes and driver metrics with tooltips for daily values. Given fewer than 7 days of activity, When viewing trends, Then an “insufficient data” placeholder is shown with guidance to complete more sessions. Given driver breakdown is opened, When the user selects a driver (on-time rate, verified sessions, reaction reliability), Then the definition and current value are shown and the last 5 contributing events are summarized without room names or host identities. Given backend computation specs, When values are rendered, Then on-time rate = on-time check-ins / scheduled check-ins (30 days), verified sessions = count of host-verified sessions (30 days and lifetime), reaction reliability = reactions sent within required windows / sessions requiring reactions (30 days); displayed values match backend within 0.5%. Given tier mapping rules are updated to v1.x, When the page loads, Then the tier label and thresholds reflect the active mapping version in config.
Actionable Coaching Tips and Upcoming Opportunities
Given the user opens the Ghost Cred dashboard, When a driver is below its improvement threshold, Then at least two actionable, contextual tips are shown with estimated impact labels (e.g., “~+5% on-time rate”) and a clear CTA per tip. Given the user has a next scheduled session within 24 hours, When the dashboard loads, Then the session appears with local start time and an on-time reminder toggle. Given the reminder toggle is enabled, When the session time is T, Then a notification is scheduled for T-10 minutes in the user’s local timezone; if OS notifications are disabled, Then an inline message explains how to enable them. Given the user has no upcoming sessions, When viewing opportunities, Then a prompt to schedule or join a recurring room is displayed with a CTA. Given a tip with a setting change is completed (e.g., enable calendar sync), When completion is detected, Then Ghost Cred is recomputed and the tips list refreshes within 5 minutes.
Lightweight Dispute Flow with Async Resolution
Given a user sees a penalty or verification they believe is incorrect, When they tap “Dispute”, Then a modal opens with reason selection, optional note (max 200 chars), and a Submit action without requesting identity documents. Given a dispute is submitted, When processing begins, Then an in-app acknowledgment appears within 1 minute and the dispute status becomes “Open”. Given a dispute is open, When resolution is completed, Then the user is notified in-app within 48 hours and the status updates to “Resolved: Upheld” or “Resolved: Reversed” with minimal metadata (session date/time, anonymized room ID) only. Given a dispute is resolved in the user’s favor, When recalculation runs, Then the associated penalty is removed and Ghost Cred updates within 5 minutes. Given any dispute-related message is displayed, When rendered, Then no counterparty identities (host names, avatars, emails) are shown.
Non-identifying, Consistent Messaging Across Platforms
Given Ghost Cred UI surfaces and notifications on iOS, Android, and Web, When the same state is displayed, Then tier names, copy strings, and numeric values are identical across platforms (allowing platform-specific UI labels only). Given any Ghost Cred surface is rendered, When inspected, Then no personal identifiers (name, email, phone, avatar) are present; only privacy-preserving signals are shown. Given deep links are used to open Ghost Cred screens across platforms, When navigation completes, Then the destination screen shows the same content and state as the source, including tier and eligibility. Given an A/B test is active on copy, When variants are shown, Then neither variant includes personal identifiers and both reference the same tier and metric values.
Localization Coverage and Clarity
Given the app language is set to a supported locale (en, es, fr, de, pt-BR, ja), When any Ghost Cred surface is viewed, Then 100% of user-facing strings are localized with no fallback markers and no truncation on common device widths (375–430pt, 360–412dp). Given pluralization or gendered grammar is required, When strings render, Then ICU MessageFormat rules are applied correctly per locale. Given a non-supported locale is selected, When viewing Ghost Cred, Then the content gracefully falls back to English without broken placeholders. Given right-to-left support is enabled for RTL locales in the future (e.g., ar, he), When toggled in QA, Then layouts mirror correctly and copy remains non-identifying.

Safe Reveal

Opt-in, time-bound unmasking for edge cases like prize claims, safety checks, or team roll calls. Requires your explicit consent, scopes who can see you, and auto-recloaks after a set window. You control when, who, and for how long—no ongoing link to other rooms or past activity.

Requirements

Explicit Consent Gate
"As a privacy-conscious user, I want to explicitly approve each reveal with a clear summary so that I stay in control of what is shared, with whom, and for how long."
Description

Provide a clear, interruptive consent flow that users must actively accept before any Safe Reveal occurs. The modal summarizes what will be revealed, to whom, and for how long, and requires an explicit confirm action (no implicit or default opt-ins). Include granular toggles for fields (e.g., display name, avatar) and a concise risk overview. Persist a consent receipt (timestamp, scope, duration) for the user and moderation audit while minimizing data retention. Block programmatic or remote reveals without local user action. Integrates with identity, permissions, analytics, and audit services; supports accessibility and localization.

Acceptance Criteria
Interruptive Consent Modal Display and Summary
Given a user initiates Safe Reveal from any room or profile context When the consent modal opens Then it must block all background interaction (focus trap, inert background) until the user acts And it must clearly summarize what fields could be revealed, to whom (scope label), and for how long (duration label) And it must display a concise risk overview capped at 200 characters without requiring scroll And it must not pre-select any revealing action or imply default opt-in
Field, Scope, and Duration Selection Controls
Given the consent modal is open When the user views available controls Then granular toggles for revealable fields (at minimum: display name, avatar) are present and default OFF And a scope selector allows choosing specific viewers/roles/groups resolvable via permissions/identity services And a duration selector offers presets (5m, 15m, 1h) and a custom option up to a max of 24h And the live summary reflects the current field, scope, and duration selections And the Confirm action remains disabled until at least one field, one scope, and one duration are selected
Explicit Confirm and Safe Cancel Behavior
Given required selections are complete When the user explicitly activates Confirm (click/tap/Enter on a focused button labeled "Reveal") Then the reveal begins and an analytics event "consent_granted" is sent without PII beyond: receipt_id, scope_type, duration_seconds, fields_count And when the user activates Cancel (or presses Esc or dismisses the modal) Then no data is revealed, no analytics "consent_granted" is sent, and the state remains cloaked And on initial modal open, keyboard focus is on Cancel, not Confirm And there is no auto-timeout path that results in reveal without explicit user action
Consent Receipt Persistence and Data Minimization
Given the user confirms When the reveal is initiated Then a consent receipt is created with: receipt_id, user_id (pseudonymous), room_id/context, selected_fields, scope_identifiers, start_timestamp_utc, end_timestamp_utc, client_device_id, app_version And the receipt is encrypted at rest and retrievable by the user in Settings > Privacy > Consent Receipts within 5 seconds And the audit service can fetch the receipt by receipt_id with proper authorization, with viewer PII redacted to group/role identifiers And receipts are automatically deleted within 30 days after end_timestamp unless a moderation hold is active And the user can export an individual receipt as JSON without revealing other users’ PII
Time-Bound Auto-Recloak and Access Revocation
Given a reveal is active When the end_timestamp is reached Then all revealed fields are re-cloaked within 2 seconds client-visible time And subsequent fetches by scoped viewers for those fields return 403/permission denied within 2 seconds And no links or references to other rooms or past activity are created or persisted during or after the reveal And a "recloak_completed" analytics/audit event is emitted
Programmatic Reveal Blocking Without Local User Action
Given no local consent has been granted within the last 60 seconds on the requesting device When any API, webhook, bot, or remote admin attempts to trigger a reveal Then the request is rejected with 401 or 423 and logged to audit with reason "no_local_consent" And reveals require a signed, single-use consent token bound to device_id, expiring in 2 minutes, produced only by the local modal Confirm action And replayed or cross-device tokens are rejected and logged And if the device is locked or the app is backgrounded, the reveal cannot proceed until the user brings the app to foreground and confirms
Accessibility and Localization Compliance
Given the consent modal is rendered When tested with keyboard-only and screen readers Then all interactive elements are reachable in logical order, have ARIA labels, and meet WCAG 2.2 AA contrast And error/help text is conveyed via role=alert or aria-live without relying on color alone And the layout supports 320px width without clipping or overlap And all strings are sourced from localization keys with available translations for at least en, es, de; RTL rendering is supported where the app supports RTL; fallback to en when a key is missing And date/time and pluralization are localized per user locale
Scoped Audience Selector
"As a participant in a live room, I want to pick exactly who can see me when I reveal so that I only share my identity with the necessary people."
Description

Allow users to precisely choose who can see their unmasked identity during a Safe Reveal. Support scopes such as: current room hosts/moderators, specific members, organization/team, or a custom list from the current room. Default to the minimal viable scope (e.g., host-only) and require explicit expansion. Enforce scope on the backend with permission checks tied to the active room/session; disallow any visibility beyond the selected scope and prevent any link to other rooms or past activity. Provide a fast, searchable UI with role chips, and ensure scope details are displayed in the consent gate and receipts.

Acceptance Criteria
Default Host-Only Scope on Safe Reveal
Given the user opens the Safe Reveal scope selector in an active room with at least one host When the selector initializes Then "Host(s) only" is the preselected scope and no broader scope is preselected And the API preview/payload shows scope_code = "HOSTS" until the user explicitly changes it And expanding scope requires an explicit user action (e.g., adding roles/members) And previously used scopes are not auto-applied to the new session
Scope Options Availability by Room Context
Given the user opens the Safe Reveal scope selector during an active room session When the scope options are displayed Then the following options are available: Hosts/Moderators, Specific Members (current room), Organization/Team (if the user has an org/team), Custom List (current room) And options that are not applicable (e.g., no org/team) are disabled with an explanatory tooltip And each option displays the resulting recipient count before confirmation
Search and Role Chips Performance
Given a room with 1,000 participants is loaded in the selector When the user types in the recipient search box Then filtered results render within 300 ms per keystroke on median hardware And role chips (Host, Moderator, Member) are visible and tapping a chip filters the list within 200 ms And selections persist across searches and pagination without loss And search matches on display name, handle, and email domain (if present), case- and diacritic-insensitive
Backend Permission Checks Enforce Selected Scope
Given a Safe Reveal session with scope S tied to room_id R and reveal_session_id RS starts at T0 When a client outside scope S or outside room_id R requests the user's identity during [T0, T_end] Then the API returns masked identity with HTTP 403 (or error SAFE_REVEAL_SCOPE_DENIED) and emits no identity data over realtime channels And when a client within S in R requests during [T0, T_end] Then the API returns the unmasked identity And all access decisions are audit-logged with room_id, RS, requester_id, decision, and timestamp
Custom List Limited to Current Room Members
Given the user selects the "Custom List" scope option When they search for and add recipients Then only current room members appear in search results And attempts to add any user not in the current room are blocked with inline error "Only members of this room can be added" And the request payload includes only member_ids from room_id R And the server rejects any out-of-room ID with 422 SAFE_REVEAL_RECIPIENT_INVALID
Consent Gate Shows Scope and Duration
Given the user has selected a scope and duration for Safe Reveal When the consent gate is displayed prior to starting the reveal Then it shows a plain-language summary: who (roles or names), how many recipients, duration, and room name And it lists up to 5 specific recipients with "+N more" if applicable And changing scope or duration updates the summary and counts in real time And the confirm action remains disabled until the user checks an explicit consent checkbox
Auto-Recloak and Post-Session Isolation
Given a reveal session starts at T0 with duration D, scope S, and room_id R When the earlier of T0 + D or room termination occurs Then the system immediately revokes visibility for all users and subsequent UI/API requests return masked identity And the session receipt shows start/end timestamps, scope S, recipient count, and room_id R And identity access attempts from any room ≠ R or outside [T0, T0 + D] are denied and logged
Time-Window Auto-Recloak
"As a user who only wants to be visible briefly, I want my reveal to auto-expire and recloak without me doing anything so that I don’t stay exposed longer than intended."
Description

Enable users to set a fixed reveal duration (e.g., 5–60 minutes) with a visible countdown, pre-expiry reminder, and controls to extend or end early. Automatically recloak at expiry regardless of app state (foreground, background, offline) using server-enforced timers and idempotent jobs. Immediately revoke access tokens and cached reveal data at expiry. Ensure resilience to network loss, app restarts, and device changes. Display clear state changes to all scoped viewers and log the end event in the audit trail. No data remains accessible after the window closes.

Acceptance Criteria
Set Reveal Duration and Countdown Display
- User can set reveal duration between 5 and 60 minutes (inclusive) in 1-minute increments before starting. - Upon start, a countdown (mm:ss) is visible to the revealer and scoped viewers, sourced from server time. - Countdown updates at least once per second when the screen is active and remains accurate after app background/foreground or device time changes (uses server time on resume). - Countdown continues to reflect correct remaining time across app restarts and across the revealer’s devices. - If duration selection is out of range, the confirmation action is disabled and an inline error is shown.
Pre-Expiry Reminder and Actionability
- A pre-expiry reminder is sent at the configured lead time (default 60 seconds) before expiry, originating from the server. - Reminder is delivered to all active devices of the revealer; duplicates are suppressed per device. - Reminder presents actionable options: Extend and End Now; actions require server acknowledgement before state changes. - If the device is offline, the reminder is queued and delivered only if the window has not yet expired; if expired, no stale reminder appears. - Taking Extend from the reminder updates the countdown immediately upon server success; if server rejects (policy/expired), the UI shows a clear error and the state remains unchanged.
Server-Enforced Auto-Recloak on Expiry
- At the exact server-defined expiry time, the system triggers auto-recloak via an idempotent server job. - All access tokens tied to the reveal window are revoked immediately; subsequent protected API calls return 403 within 2 seconds. - Scoped viewers lose access to revealed data within 5 seconds of expiry across foreground, background, offline, or terminated app states. - Multiple job retries or duplicate events do not create duplicate notifications or audit entries; final state is Recloaked. - Local client countdowns may drift but final recloak is governed by server time with maximum observed drift ≤5 seconds.
Manual Early End and Extend Controls
- Revealer can invoke End Now at any time before expiry; upon server success, the reveal recloaks within 2 seconds and the countdown stops. - Revealer can Extend before expiry; upon server success, a new expiry is set and countdown updates immediately across all of the revealer’s devices. - Extension amount respects policy-configured limits; attempts outside policy are rejected with a clear inline error and no state change. - Race conditions near expiry are handled atomically: if Extend and Expiry collide, server resolves deterministically and returns the final state; client reflects it within 3 seconds. - All manual actions are logged with actor, action (end/extend), previous expiry, new expiry (if any), and reason.
Access Revocation and Cache Invalidation
- After recloak (expiry or manual), any attempt by scoped viewers to read revealed profile/media returns 403 (or 410 for deep links) from the server. - Client purges locally cached reveal content and thumbnails within 5 seconds of learning the final state (immediately on online devices; on offline devices at next launch or reconnect before rendering). - Previously issued signed URLs or media links become unusable immediately after expiry; fetch attempts return non-success and are not auto-refreshed. - Offline viewers cannot view previously cached reveal content; the UI displays a generic "No longer available" state without rendering sensitive data. - No revealed data appears in notifications, widgets, or previews after recloak; stale notifications tap through to a non-accessible state.
Viewer State Change Broadcast and Audit Trail
- Upon recloak, scoped viewers’ UIs transition from Revealed to Recloaked state within 3 seconds with a clear status message and timestamp. - Non-scoped users receive no state change notification and cannot observe any reveal-specific indicators. - An audit log entry is created exactly once per reveal window end with fields: revealId, userId, scopeId, startTime, endTime, endCause (expiry/manual), serverTimestamp (UTC). - Audit log is queryable within 10 seconds of the event and shows consistent values across retries; no duplicate or contradictory entries appear. - All broadcast and audit operations succeed even if the revealer’s device is offline or the app is terminated.
Resilience Across Network Loss, Restarts, and Device Changes
- If the revealer goes offline during the window, the server timer still ends the window on schedule; on reconnect, the client immediately reflects the final state. - Killing or restarting the app does not extend or reset the window; on relaunch, countdown/state is restored from server within 3 seconds. - Signing in on a new device during an active window shows the correct remaining time within 3 seconds of successful sync; after expiry, all devices show Recloaked. - Deep links to an expired reveal return 410 Gone and do not rehydrate any reveal data on the client. - System clock changes on client devices do not affect server-determined expiry or token validity; countdown corrects on next server sync.
Ephemeral Alias & Non-Linkability
"As a cautious user, I want my reveal to use a temporary alias that can’t be tied to my other rooms or history so that my privacy remains protected after the window ends."
Description

Issue an ephemeral alias that maps to the user’s real identity only for the selected scope and time window. Rotate the alias per reveal request and per room, and prevent cross-room or historical linking. Restrict data exposure to the chosen fields (e.g., display name/avatar) and block access to any past activity or other rooms. On expiry, delete or tombstone the alias mapping and invalidate related caches and search indices. Enforce non-linkability in APIs and UI (no backlinks, no profile drill-through). Integrates with identity, caching, and search services with privacy-by-design defaults.

Acceptance Criteria
Scoped Time-Bound Reveal with Explicit Consent
Given a signed-in user opts into Safe Reveal and selects Room R, Audience S, and expiry time T When they confirm consent Then the system issues an ephemeral alias A unique to (user, R, request) and stores mapping with expiry T And only members of S can resolve A to the selected fields during [now, T] And any access to A by non-scoped users or outside [now, T] returns 403 (unauthorized) or 404 (not found) And if the user revokes consent, A is invalidated within 5 seconds and future accesses return 404
Alias Rotation Per Room and Per Request
Given the user has one or more prior reveals (aliases A1..An) When the user initiates a new reveal in any room (including R) creating alias Anew Then Anew != any of {A1..An} and cannot be inferred from them And API responses for Anew contain no stable identifiers (e.g., userId, global handle, immutable link) that also appear for {A1..An} And UI for Anew provides no controls or affordances that expose or navigate to {A1..An}
Restricted Field Exposure Only
Given the user selects allowed fields F = {displayName, avatar} for the reveal When scoped viewers retrieve alias A Then the API response includes only F and excludes PII and metadata: email, phone, userId, handles, streak history, room memberships, permissions, device info And the UI renders only F with no additional inferred data (e.g., initials from email) And any attempt to query additional fields via API returns 403 or null values
Non-Linkability in API and UI
Given alias A is active When a scoped viewer inspects A in UI or via API Then no backlinks, profile URLs, or drill-through endpoints to the real profile are present And clicking name/avatar opens no profile view, and no "more from this user" or "other rooms" links are shown And deep links use only /reveal/A without any underlying user identifiers
No Access to Past Activity or Other Rooms
Given alias A is active in Room R When a viewer attempts to access A's past activity, streaks, check-ins, or content outside the reveal window or in rooms R' != R Then the system returns 403/404 or empty results with no pagination tokens And room/member directories outside R never list A And notifications outside scope are not generated for A
Expiry, Tombstoning, and Invalidation
Given alias A has expiry time T When current time >= T Then the A->user mapping is deleted or tombstoned And caches and search indices for A are invalidated within 60 seconds And subsequent API requests to A return 404 with tombstone code, and UI updates within 60 seconds to reflect expiry And eventual consistency is achieved within 2 minutes across all regions
Search and Discovery Constraints
Given alias A is active with scope (R, S) and window [t0, T] When users perform global, room, or autocomplete search Then only members of S see A in results, and only within R and [t0, T] And search snippets contain only allowed fields and no profile links And after T, A is removed from all search and autocomplete within 60 seconds
Reveal Request & Notification Flow
"As a participant, I want to receive clear reveal requests that I can accept or modify so that I can complete prize claims or roll calls without giving up unnecessary privacy."
Description

Provide a standardized request workflow for prize claims, safety checks, and roll calls. Allow hosts/moderators (or designated roles) to send a reveal request with reason, proposed scope, and suggested duration. Notify the user via in-app prompt and push; let the user accept, narrow scope, shorten duration, or decline. On acceptance, trigger consent gate and start the time window. Display context-sensitive banners to scoped viewers (e.g., “Revealed to Host for 15m”). Log request/response outcomes to audit while respecting data minimization. Support reminders and ability to snooze or block specific requesters.

Acceptance Criteria
Host/Moderator Sends Reveal Request
Given a host or moderator is in a room with reveal permissions When they initiate a reveal request Then the request requires a reason, a proposed scope, and a suggested duration (1–60 minutes) And if any required field is missing or invalid, the UI blocks submission with inline validation And when submitted with valid inputs, the system creates a Pending request with a unique ID and timestamp and does not change visibility
In-App Prompt and Push Notification Delivery
Given a valid Pending reveal request targeting a user When the request is sent Then the target user sees an in-app modal within 2 seconds if active and receives a push notification within 10 seconds if backgrounded And the prompt/push includes reason, requester role/name, proposed scope label, and suggested duration And tapping the notification deep-links to the decision screen within 3 seconds And no other users receive notifications
User Decision: Accept, Narrow, Shorten, or Decline
Given the user opens the reveal decision screen for a Pending request When choosing a response Then they can Accept as proposed, Narrow the scope to a subset of the proposal, Shorten the duration to ≤ proposed (1–60 minutes), or Decline And the UI prevents expanding scope beyond the proposal; empty scope disables Accept And on Accept (with or without changes), the requester is notified of final scope/duration and the audit log records proposal vs consented values And on Decline, the requester is notified, status becomes Declined, and no visibility change occurs
Consent Gate and Timer Start
Given the user selects Accept on a reveal request When the consent gate is shown Then explicit confirmation (toggle + Confirm) is required to activate the reveal And on Confirm, the reveal activates immediately and a countdown starts for the consented duration And if Cancel is chosen, the request remains Pending with no visibility change And the consent event is logged with timestamp and final scope/duration; scoped viewers gain visibility within 2 seconds
Scoped Visibility and Context Banners
Given an active reveal is in effect Then only users in the consented scope see the user’s identity/avatar in the current room; all others see the cloaked state And scoped viewers see a banner chip "Revealed to [Scope Label] for [mm:ss]" with a live countdown; the revealed user sees a persistent timer indicator And banners/indicators update each second and disappear within 2 seconds after expiry or manual re-cloak
Auto Re-Cloak and No Cross-Room Linkage
Given an active reveal is in effect When the timer expires or the user manually re-cloaks Then visibility reverts to cloaked state immediately and no further visibility persists And the reveal applies only to the originating room; other rooms, past messages, and activity history remain cloaked during and after the reveal And attempts to access identity via cached UI after expiry show cloaked state; banners are removed
Audit Logging, Reminders, Snooze, and Block
Given any reveal request lifecycle event (created, reminded, accepted, modified, declined, expired, blocked) Then the system logs eventType, requestId, requester role, requesterId, targetUserId, reason category, scope label/size, proposed and consented durations, timestamp, and outcome; no message content or location metadata is stored And access to logs is restricted to authorized admins; PII is redacted in analytics exports by default And for Pending requests, reminders are sent at 5 and 15 minutes unless snoozed or blocked; snooze defers all notifications for 30 minutes; blocking a requester prevents future requests and suppresses current reminders, and the requester sees "Reveal unavailable" And all reminder, snooze, block, and unblock actions are captured in the audit log, and the user can unblock from settings
Abuse Prevention & Rate Limiting
"As a user, I want limits and controls around reveal requests so that I’m not spammed or coerced into revealing my identity."
Description

Protect users from harassment and overuse by enforcing per-requester and per-room rate limits, cooldown periods, and daily caps on reveal requests. Provide block/report controls, Do Not Disturb honoring, and organization-level quotas. Require reason codes for requests and surface them in the consent gate. Detect and halt bulk or automated request patterns. Ensure all reveals require local user interaction; no silent or auto-accepted paths. Expose admin moderation tools with aggregated metrics while preserving user privacy. Integrate with trust & safety pipelines and API throttling at the edge.

Acceptance Criteria
Per-Requester Cooldown and Daily Cap
Given requester A in room R targets user B When A sends a reveal request Then the system enforces a minimum 15-minute cooldown between A→B requests in room R Given A has sent reveal requests to B in room R within the last 24 hours When A attempts another request Then no more than 3 total A→B requests in room R are permitted per rolling 24 hours, and excess requests are rejected with HTTP 429 and a Retry-After header Given a request is rejected due to cooldown or cap Then the response includes error code REVEAL_RATE_LIMITED and the next eligible timestamp is displayed to A in the UI
Room- and Organization-Level Throttles and Caps
Given reveal requests are being sent in room R When the total count exceeds 30 requests in any rolling 5-minute window Then subsequent requests are throttled with HTTP 429, reason ROOM_THROTTLED, and metrics are emitted for room R Given room R has accumulated reveal requests today When the total reaches 300 in a rolling 24-hour window Then additional requests to room R are rejected with HTTP 429 and Retry-After reflecting the window reset Given an organization has a daily reveal quota configured (default 1000/day) When the org reaches 90% of quota Then a soft warning is surfaced to org admins; at 100% further requests are rejected with HTTP 429 ORG_QUOTA_EXCEEDED Given requests originate from mobile, web, or public API Then the same throttles and caps are enforced consistently across all entry points
Reason Codes Required and Surfaced in Consent Gate
Given a requester composes a reveal request When submitting the request Then a reason_code from {prize_claim, safety_check, team_roll_call, other} is required; if reason_code is other, free-text reason must be 10–280 characters Given a request is missing a valid reason_code or violates free-text constraints Then the system rejects it with HTTP 400 INVALID_REASON_CODE and no notification is sent to the target Given a reveal request is delivered to target B Then the consent gate displays the selected reason_code and free-text reason exactly as provided; no additional requester metadata is revealed beyond room context Given a valid request is submitted Then the reason_code and free-text are written to an immutable audit log and included in admin aggregate metrics; they are not visible to non-target members
Do Not Disturb and Block/Report Enforcement
Given target B has Do Not Disturb enabled When requester A attempts a reveal request to B Then the request is not delivered or queued; A receives a generic "User unavailable" message; B receives no notification; an audit event is logged Given B has blocked A When A attempts any reveal request to B (any room) Then the system returns HTTP 403 BLOCKED, delivers no notification to B, and records the attempt Given B opens a reveal request from A When B taps Report and selects a category Then the request is hidden from B, an optional Block A prompt is shown, and a Trust & Safety case is created and queued within 10 seconds with the request metadata and reason
Bulk/Automated Request Detection and Edge Throttling
Given an account or device submits >10 reveal requests to ≥10 distinct targets within 60 seconds When additional reveal requests are attempted in that period Then the system blocks further reveal requests from that account/device for 60 minutes, responds with HTTP 403 AUTOMATION_SUSPECTED, and creates a T&S alert Given a reveal request is received without a valid client-side interaction token issued within the last 5 seconds Then the request is rejected with HTTP 400 MISSING_INTERACTION and counted in automation-detection metrics Given requests are arriving at the edge from a single IP/device fingerprint When reveal-related endpoints exceed 60 requests/minute Then the edge returns HTTP 429 with a Retry-After header and logs a throttle event
Local User Interaction Required for All Reveals
Given target B receives a reveal request When B is not in the foreground app or does not explicitly tap Accept in the consent gate Then no unmasking occurs and the request remains pending until expiry Given the consent window elapses (default 5 minutes) without acceptance Then the request auto-expires, the requester is notified of expiry, and no data is revealed Given B taps Accept Then unmasking is applied only to the scoped audience and for the selected duration, and the system auto-recloaks immediately when the timer ends; no server-to-server or webhook path can bypass this flow
Admin Moderation Tools with Aggregated, Privacy-Preserving Metrics
Given a user has the Trust & Safety Admin role When accessing the moderation dashboard Then they can view aggregate counts (requests sent, accepted, rejected, rate-limited, blocked, reported) by organization, room, and 15-minute windows without exposure of usernames, emails, or user IDs Given an admin adjusts per-org or per-room quotas or throttles When saving changes Then updates take effect within 5 minutes, are RBAC-enforced, and an audit log records actor, timestamp, prior value, new value Given an admin exports metrics When selecting a time range and dimensions Then only aggregated data meeting a k-anonymity threshold of ≥5 is exportable; attempts below threshold are denied with HTTP 403 PRIVACY_THRESHOLD Given any admin action is performed Then an immutable audit trail is stored for 1 year and is searchable by T&S staff

Ghost Guard

Room-level privacy and moderation controls: set anonymity level (full ghost, pseudonym-only, or selective reveal), cap message frequency, rate-limit reactions, and mute/report ghosts without learning their identity. Clear privacy rules are shown at join so expectations are obvious and abuse is curbed.

Requirements

Anonymity Modes Engine
"As a room host, I want to choose the room’s anonymity level so that participants feel safe while expectations are clear and enforceable."
Description

Provide room-level controls to select one of three privacy modes—Full Ghost (no profile details visible), Pseudonym-only (room-scoped nickname/avatar with no link to the real account), and Selective Reveal (participants may optionally reveal to the room or only to the host under predefined triggers). The system must enforce the chosen mode consistently across presence lists, message metadata, reactions, streak displays, and leaderboards to prevent identity leakage. Implement identity escrow that assigns each participant a stable, room-scoped anonymized ID usable for moderation and analytics without exposing real identities to hosts or participants. Integrate the selector into room creation/edit flows with safe defaults, persist settings across sessions, and apply them to existing members on change. Disable DMs and user mentions in Full Ghost, constrain profile deep-links in Pseudonym-only, and support a consent-based reveal flow in Selective Reveal with clear audit trails.

Acceptance Criteria
Default Mode at Room Creation
Given a user opens the Create Room flow When they view the privacy selector Then the default selection is Full Ghost And the selector is required before room creation can be completed And the selected mode is displayed on the pre-join screen for invitees And upon room creation, the chosen mode is persisted to storage and retrievable via API and UI after app restart
Mode Persistence and Application on Edit
Given an existing room with active participants and a current anonymity mode When the host changes the anonymity mode and saves Then the new mode is applied to all existing and new members within 2 seconds And presence lists, message metadata, reactions, streak displays, and leaderboards immediately reflect the new mode’s visibility rules And no historical content is retroactively modified to reveal additional identity information And the updated mode persists across app restarts and is returned by the room settings API
Full Ghost Enforcement Across All Surfaces
Given a room is in Full Ghost mode When any participant joins, posts a message, reacts, checks in, or appears on a leaderboard Then no display name, handle, avatar, or profile deep-link is visible in the UI for that participant And event payloads and exports contain only room-scoped anonymized IDs (no account IDs, emails, or profile URLs) And presence lists show no identifiable profile details And auditing confirms zero occurrences of personal identifiers in logs for these events
Pseudonym-only Visibility and Deep-link Constraints
Given a room is in Pseudonym-only mode When a participant interacts (presence, message, reaction, streak, leaderboard) Then only the room-scoped nickname and room-scoped avatar are shown And there are no links or navigations to the participant’s real profile from any UI element And profile deep-link endpoints return 403/blocked when invoked from this room context And event payloads exclude real account identifiers, including user ID, email, or global profile URL
Selective Reveal Consent and Audit Trail
Given a room is in Selective Reveal mode When a host-initiated trigger or participant-initiated action requests identity reveal Then a consent dialog presents scope options (reveal to room, reveal to host only) with the exact data to be shared And no identity data is revealed unless the participant explicitly confirms And the system records an immutable audit entry with timestamp, actor, trigger, scope, fields revealed, and room ID And the chosen visibility persists across sessions and is visible in the audit log to the host and the participant
Identity Escrow and Stable Room-Scoped IDs
Given any anonymity mode is active When a participant joins a room for the first time Then the system assigns a stable, unique, room-scoped anonymized ID And the same participant receives the same anonymized ID on rejoin in the same room and a different ID in other rooms And moderation actions (mute, report, ban) and analytics operate solely on the anonymized ID without exposing real identity to hosts or participants And API responses for moderation and analytics never include real account identifiers
Full Ghost: DMs and Mentions Disabled
Given a room is in Full Ghost mode When a participant attempts to start a DM from the room or type an @mention Then the UI provides no entry points for DMs and blocks @mentions with an inline notice And the server rejects any DM or mention attempts via API with 403 and a privacy-mode error code And no notifications or message artifacts are created as a result of these attempts
Message Frequency Caps
"As a moderator, I want to cap how often users can post so that spam and disruption are minimized without constant manual policing."
Description

Enable server-enforced per-room limits on how often a participant can post messages, with configurable thresholds (e.g., X messages per Y minutes) and preset templates for quick setup. Provide role-based exceptions for hosts and moderators, and display real-time, client-side feedback (cooldowns and remaining quota) to reduce confusion. Ensure limits are enforced across all clients, are resilient to clock skew and reconnects, and are logged for auditability and tuning. Expose configuration via room settings, surface violations as gentle toasts, and capture telemetry to help optimize defaults without storing personally identifying content.

Acceptance Criteria
Server-Enforced Cap with Real-Time Cooldown Feedback
Given a room cap of 3 messages per 5 minutes for members and a member without exceptions When the member attempts to send a 4th message within the same 5-minute window from any client Then the server rejects the message with a RATE_LIMITED error that includes next_allowed_at (UTC ISO8601) and remaining_quota=0 And the message is not persisted or broadcast And the client shows a non-blocking toast indicating the rate limit, remaining time until next_allowed_at (in seconds), and the active policy (e.g., “3 per 5m”) And a visible cooldown indicator counts down in real time to 0 And after next_allowed_at elapses, the next message succeeds without requiring app restart
Role-Based Exceptions for Hosts and Moderators
Given a room cap of 3 messages per 5 minutes for members, with exceptions configured as hosts: unlimited and moderators: 10 per 5 minutes When a host sends 6 messages within 5 minutes Then the server accepts all 6 messages and no cooldown or warning is shown When a moderator sends an 11th message within 5 minutes Then the server rejects the 11th message with RATE_LIMITED including next_allowed_at and remaining_quota=0 And a standard member is still capped at 3 messages per 5 minutes and is rate-limited on the 4th
Preset Templates Applied via Room Settings
Given the host opens Room Settings → Ghost Guard → Message Frequency Caps And templates are visible: Light (3 per 5m), Standard (5 per 10m), Strict (1 per 2m), and Custom When the host selects Standard and saves Then the effective cap becomes 5 messages per 10 minutes for members And any role-based exceptions remain unchanged unless explicitly edited And the new cap is enforced within 5 seconds across all connected clients And reopening settings shows Standard selected and the numeric thresholds populated accordingly And an audit event is recorded with room_id, actor_role=host, template=Standard, thresholds, and timestamp
Resilience to Client Clock Skew
Given a member device clock is 10 minutes ahead or behind server time When the member is rate-limited and the server returns next_allowed_at Then the client uses server-provided next_allowed_at to compute remaining cooldown And changing the device clock does not allow bypassing the cap And locally displayed timers adjust to server time on reconnect or foreground without drifting And the next successful post occurs only at or after next_allowed_at per server time
Quota Persistence Across Disconnects and Device Switch
Given a member is rate-limited on web at T0 with next_allowed_at = T0 + 5m When the member force-quits the web app, opens the iOS app, and attempts to post before next_allowed_at Then the server rejects the post with the same next_allowed_at and remaining_quota=0 And the iOS client fetches and displays the remaining cooldown within 1 second of the failed attempt or on room join And after next_allowed_at, the first post succeeds and a new window is started correctly
Cross-Client Enforcement Consistency
Given a member is concurrently connected on web and iOS with a cap of 2 messages per 1 minute When the member sends 2 messages from web within 60 seconds Then a 3rd message sent from iOS within the same 60 seconds is rejected with RATE_LIMITED And both clients display synchronized remaining_quota=0 and identical next_allowed_at (±1s) And after 60 seconds elapse from the first message, both clients can send again and remaining_quota resets to 2
Audit Logging and Telemetry Without PII
Given message cap violations and configuration changes occur in a room When such an event happens Then an audit/telemetry record is emitted containing room_id, user_role, anonymized_user_token (non-reversible), thresholds or template, event_type, and timestamp And the payload excludes message content, real user identifiers (e.g., email, username), and IP addresses And the event is queryable in the analytics store within 2 minutes of occurrence And a privacy check on sampled events confirms absence of PII And aggregate metrics (e.g., violations per room per day) can be produced for tuning
Reaction Rate Limiter
"As a participant, I want reactions to be rate-limited in busy rooms so that my feed remains readable and not flooded by spam."
Description

Implement burst and sustained rate limits for reactions per user per room to prevent reaction spam while preserving lightweight engagement. Enforce limits on the server with sliding windows and short cooldowns, apply role-based relaxations where appropriate, and return clear, low-latency signals to the client for UX feedback. Ensure limits cover all reaction entry points (message-level and room-level), are compatible with offline buffering, and are observable via metrics and logs for tuning without exposing identities.

Acceptance Criteria
Burst Limit Enforcement (Per User, Per Room)
Given a member user in room R with burst_limit=5 per 3s and cooldown=2s When the user sends 6 reactions within the same 3-second sliding window via any reaction entry point Then the first 5 reactions are accepted and persisted, and the 6th is rejected with code=rate_limited and reason=burst And the server response includes limit_type=burst, limit_count=5, window_seconds=3, remaining=0, cooldown_ms=2000 And the next reaction attempted after the 2-second cooldown is accepted
Sustained Sliding-Window Limit Enforcement
Given a member user in room R with sustained_limit=30 per 60s (sliding window) and cooldown=2s When the user attempts the 31st reaction within 60s of the first Then the 31st reaction is rejected with code=rate_limited and reason=sustained And the response includes limit_type=sustained, limit_count=30, window_seconds=60, remaining=0, retry_after_ms > 0 And after waiting retry_after_ms (or cooldown_ms, whichever is longer), the next reaction is accepted And as the sliding window advances and older reactions age out, remaining increases accordingly
Unified Limits Across Message- and Room-Level Reactions
Given a member user in room R with burst_limit=5 per 3s and sustained_limit=30 per 60s When the user sends reactions across both entry points (message-level and room-level) within the same windows (e.g., 3 message-level + 2 room-level within 3s) Then the total counts are aggregated per user per room across entry points for rate-limit evaluation And any subsequent reaction of either entry point that exceeds the active limit is rejected with the appropriate reason (burst or sustained) And responses for both entry points use the same structured fields (code, reason, remaining, cooldown_ms/retry_after_ms)
Offline Buffered Reactions Sync
Given the client has offline buffering enabled and queues 10 reactions while disconnected with preserved client_generated_ids and timestamps When connectivity is restored and the client flushes the buffer to room R Then the server processes reactions in timestamp order, applies per-user per-room sliding-window limits, and accepts only those within limits And each reaction receives an individual ack/nack with code=ok or code=rate_limited and reason in {burst,sustained} And rejected reactions are not persisted or double-counted; accepted reactions are idempotent based on client_generated_id And the server includes cooldown_ms/retry_after_ms for rejected items to guide client retry scheduling
Role-Based Rate-Limit Relaxation
Given role-based multipliers are configured as: host=2x, moderator=2x, member=1x for both burst and sustained limits in room R When a host and a member each attempt reactions at high frequency in the same room Then the host is allowed up to 2x the configured limits before receiving rate_limited responses, while the member is limited at 1x And role is resolved server-side from room membership; client-provided role is ignored And rate-limit responses do not reveal the user’s identity or role to other clients (no role or user identifiers in broadcast events)
Low-Latency Feedback and Cooldown Signaling
Given the reaction rate-limiter is deployed in production-like conditions When a user’s reaction is allowed or rejected due to limits Then the decision latency (request receipt to decision) is <= 50ms at p50 and <= 200ms at p95 And the response includes structured fields: code in {ok, rate_limited}, reason in {burst, sustained}, limit_type, window_seconds, limit_count, remaining, cooldown_ms (if applicable), retry_after_ms (if applicable) And for HTTP endpoints, status=429 is returned on rate limit with Retry-After and X-RateLimit-* headers; for realtime acks, equivalent fields are present
Privacy-Preserving Observability and Logs
Given rate-limiting is exercised in room R by anonymous/ghost users When allow and block events occur Then metrics are emitted: reactions_allowed_total and reactions_blocked_total by {room_id, reason, role}, and decision_latency_ms histogram, without user identifiers And structured logs for rate_limit_decision events exclude user_id/handle and instead include a room-scoped anonymized actor_hash and configuration (limit_type, window_seconds, limit_count, reason, remaining, cooldown_ms/retry_after_ms) And a dashboard can display counts and latencies per room without exposing identities And sampled trace/log data can be correlated via actor_hash without enabling identity reconstruction
Anonymous Moderation Actions
"As a moderator, I want to mute or report disruptive ghosts without knowing who they are so that I can keep the room safe while preserving their privacy."
Description

Allow hosts and moderators to mute, time out, remove, or report participants who are ghosted without learning their real identity. Use room-scoped anonymized handles and action tokens that target the identity-escrowed user ID, ensuring actions are precise and reversible. Provide configurable durations and reasons, immediate client feedback to the affected user, and transparent room notices that do not reveal identity. All actions must be recorded in tamper-evident audit logs and support escalation to Trust & Safety where only authorized staff can de-anonymize if required by policy. Ensure bans persist across reconnects and devices while preserving anonymity to peers and moderators.

Acceptance Criteria
Mute and Timeout a Ghost Participant
Given I am a host or moderator in a live room and a participant is ghosted under a room-scoped anonymized handle When I apply a mute or timeout with a selected duration (e.g., 5m, 1h, custom within 1m–30d) and an optional reason code Then the participant’s client is prevented from sending messages and reactions for the selected duration within that room only And the participant receives immediate in-client feedback stating the action type, remaining duration, and reason code And a room system notice announces a moderation action without revealing the participant’s identity or handle And the anonymized handle remains unchanged and unrevealed as the target of the action to other participants And the action expires automatically at the end of the duration without manual intervention
Remove and Ban Ghost Across Sessions
Given I am a host or moderator and a participant is ghosted with an escrowed user ID When I remove the participant and select Ban with a duration (e.g., 7d or Indefinite) Then the participant is immediately removed from the room and cannot rejoin from any device or session for the ban duration And any reconnect attempt is blocked server-side and logged with the same escrowed ID while preserving anonymity to peers and moderators And a generic room notice states a participant was removed/banned without revealing identity or handle And the ban is reversible via Unban, which restores access only for the original escrowed ID And reversing the action does not disclose the participant’s identity to hosts, moderators, or peers
Room-Scoped Anonymized Handles and Precise Action Tokens
Given a room with multiple ghost participants When I open the moderation panel Then each participant is represented by a room-scoped anonymized handle and a non-human-readable action token fingerprint And the same real user retains the same anonymized handle within the room across reconnects while receiving different handles in other rooms And performing an action using the token affects only the intended escrowed user ID even if the visible handle changes mid-session And re-submitting the exact same action token within 5 minutes is idempotent (no duplicate enforcement or logging of the same action)
Configurable Durations and Reason Codes
Given I initiate a moderation action (mute, timeout, remove, ban) When I configure the action Then I must select a duration within allowed bounds (1m–30d, or Indefinite for bans) with a default value preselected And I can choose a standardized reason code and optionally add free-text up to 140 characters And the selected duration and reasons are applied to enforcement and stored for auditing And room notices do not include the reason; only the affected user’s client displays the reason code label and free-text
Immediate Client Feedback and Room Notices
Given a moderation action is confirmed by the server When the action is propagated to clients Then the affected user receives a real-time in-app notice within 1 second of server acknowledgment, including action type, remaining duration, and an appeal/help link And the affected user’s compose and reaction inputs are disabled with an explanatory tooltip for the duration And other room participants see a generic system notice about a moderation action with no identity-revealing details And no push notifications or external messages disclose the target’s identity
Tamper-Evident Audit Logging
Given any moderation action (apply, reverse, expire) When the action state changes Then an append-only audit entry is written containing timestamp (UTC), actor role, action type, action token fingerprint, escrowed user ID reference, reason code, free-text, and outcome And each entry includes a SHA-256 hash chaining to the prior entry to provide tamper evidence And corrections are recorded as new entries referencing the prior entry; originals remain immutable And an audit verification endpoint can export a 24-hour log and returns PASS when the hash chain is intact, FAIL otherwise
Escalation to Trust & Safety with Controlled De-anonymization
Given a moderator files a Report on a ghost participant When the report is submitted with reason code and optional attachments Then a T&S case is created referencing the escrowed user ID and room metadata without revealing real identity to the moderator And only users with the T&S_Reviewer permission can de-anonymize, which requires a policy reason; each access is logged in the audit log And the moderator receives a redacted case ID and status updates without identity details And de-anonymization events are reviewable and must pass audit verification
Join-time Privacy Rules Banner
"As a new participant, I want to see the room’s privacy and moderation rules before I join so that I can decide whether to participate."
Description

Present a mandatory, localized banner when a user joins or re-joins a room that summarizes the current anonymity mode, what data is visible to others, whether messages are retained, the message/reaction limits, and the available moderation actions. Require explicit acceptance before participating, store consent with timestamp and versioning, and re-prompt if rules change. Ensure accessibility compliance, concise copy with a link to full policy, and a dismissible preview state that allows viewing but blocks posting until accepted. Reflect the rules consistently in tooltips and settings to align expectations and reduce abuse.

Acceptance Criteria
Join Banner Displays Accurate Privacy Rules
- Given a user joins or re-joins a room, when the room loads, then a mandatory banner is displayed before the composer is enabled - The banner summarizes: anonymity mode, data visibility scope, message retention duration, message frequency cap, reaction rate limit, and available moderation actions (mute/report) - The banner includes a clearly labeled link to the full policy - All values shown match the room’s current server-side configuration - The banner remains visible until the user explicitly accepts or dismisses to preview-only state
Localization and Policy Link Fallback
- Banner text renders in the user’s active locale; if the locale is unavailable, fallback is English - Dates/times, numbers, and units in the banner are localized to the user’s locale - The full policy link opens the policy in the same locale or English fallback - The selected locale persists across sessions and devices for the user
Block Participation Until Explicit Acceptance
- Until the user selects Accept, sending messages and reactions is disabled - User can view room content; composer shows helper text indicating acceptance is required - After Accept, composer and reactions are enabled immediately without reload - Attempting to post before acceptance shows a non-blocking tooltip explaining the requirement
Consent Recording and Versioned Re-Prompt
- On Accept, the system records consent with user_id, room_id, rules_version, UTC ISO 8601 timestamp, and locale - Users are not re-prompted in the same room for the same rules_version - If rules_version changes, the banner reappears on next room load and participation is blocked until re-accepted - Server rejects post/reaction events without current consent; client surfaces the banner and an actionable error - Consent records are queryable by room_id and user_id for audit
Accessibility Compliance (WCAG 2.1 AA)
- Banner supports full keyboard navigation with visible focus order and indicators - Screen readers announce role, title, body, controls, and link; all controls have accessible names - Focus is trapped within the banner until Dismiss or Accept; pressing Esc closes to preview-only state - Text and interactive elements meet color contrast ratio ≥ 4.5:1 - Status and error messages are announced via an aria-live region
Consistency Across Tooltips and Settings
- Tooltips and room settings display the same anonymity mode, retention duration, message/reaction limits, and moderation actions as the banner - Values in tooltips/settings update within 5 seconds of a server-side rules change - No conflicting or outdated values are shown across surfaces during a session
Offline and Error Handling
- If offline at join, show a cached banner when available; participation remains blocked until the server confirms rules and records consent - If no cached rules exist, show a network-required message and keep participation disabled - If consent submission fails, keep composer disabled, preserve user state, and show a retry action - On reconnect, auto-refresh rules and re-prompt if the rules_version differs from last accepted
Abuse Reporting & Trust Escalation
"As a participant, I want an easy way to report abuse without exposing my identity so that harmful behavior is addressed quickly and safely."
Description

Provide an in-room reporting flow that lets participants report messages, reactions, or users while remaining anonymous to the room. Automatically capture minimal necessary context (timestamps, recent events, room settings, and anonymized IDs), rate-limit duplicate reports, and allow users to add category and notes. Route reports to an internal Trust & Safety queue with triage states, SLAs, and the ability for authorized staff to de-anonymize through identity escrow when policy requires. Notify reporters with status updates without revealing outcomes that could compromise privacy, and surface aggregate room-level safety metrics to owners.

Acceptance Criteria
Anonymous In-Room Report of a Message
Given I am a participant viewing a message in a room When I tap Report from the message actions Then a report modal opens within 500 ms And my report is submitted without revealing my identity to any room participant, the reported user, or room owner And the submission completes within 2 seconds on a good network (>=3 Mbps) And the report is associated to an anonymized reporter token not visible in client UI And the reported user receives no notification identifying the reporter
Automatic Minimal-Context Capture on Report Submission
Given a report is submitted for a message, reaction, or user When the backend receives the report Then it stores only the minimal context: content_type, content_id, room_id, room_privacy_snapshot, UTC timestamp, anonymized_reporter_token, and up to the last 10 related events within 5 minutes And it includes a content snapshot (if applicable) truncated to 1 KB And it excludes PII such as IP, email, and device identifiers by default And the persisted payload size is <= 50 KB And all fields pass schema validation and are write-once immutable
Duplicate Report Rate-Limiting per Reporter and Content
Given I have reported a specific message, reaction, or user When I attempt to report the same target again within 24 hours Then the system blocks the submission and shows a non-blocking notice explaining the duplicate limit And per reporter, total report submissions are limited to 5 per 10 minutes across a room And per target, multiple reports from the same reporter coalesce into one queue item with an incremented count And rate-limit decisions are logged with reason codes
Report Category Selection and Optional Notes
Given I open the report modal When the modal renders Then I must select exactly one category from a predefined list: Spam, Harassment/Bullying, Hate/Violence, Self-harm/Safety, NSFW, Impersonation, Other And Notes is optional up to 500 characters and supports plain text only And if Other is selected, Notes becomes required with a minimum of 10 characters And the Submit button remains disabled until all required fields are valid And client- and server-side validation errors are shown inline within 200 ms
Routing to Trust & Safety Queue with Triage States and SLAs
Given a report is successfully submitted When it is ingested by the Trust & Safety service Then a queue item is created with states: New, Triage, Investigating, Actioned, Closed And priority is assigned by category mapping (e.g., Self-harm/Hate=P0, Harassment=P1, Spam=P2) And SLAs are enforced: P0<=2h, P1<=24h, P2<=72h to first action And overdue items trigger an on-call alert and are flagged Breach in the queue UI And duplicate reports on the same target coalesce into one case with a visible reporter_count And staff can assign owner, add internal notes, and change state with audit logs
Authorized Staff De-anonymization via Identity Escrow
Given a Trust & Safety staff member is viewing a queue item When they request de-anonymization Then access is permitted only for role=TrustAdmin with active 2FA and a required justification And the request is evaluated against policy tags (e.g., P0 harm, law-enforcement request) and denied if not required And identity is returned via the escrow service with minimal attributes (account_id and verified contact hash) only And the action writes an immutable audit log with requester, timestamp, case_id, justification, and returned fields And no information is emitted to room participants or the reported user as a result of this action
Reporter Status Updates and Owner Aggregate Safety Metrics
Given I have submitted a report When the report lifecycle changes state Then I receive status updates: Received (immediate), In Review (on state change), Closed (on resolution) via in-app inbox, with optional push if enabled And updates do not reveal staff actions, reported user identity, or outcomes beyond generic labels And room owners see aggregated, privacy-preserving metrics for the last 30 days: total reports, by category, open count, median time-to-first-action And metrics are shown only when counts per bucket >=3 to prevent re-identification And metrics refresh within 15 minutes and contain no reporter or reported user PII
Moderator Roles & Permissions Model
"As a room owner, I want to delegate moderation within clear limits that protect participant anonymity so that others can help manage the room safely."
Description

Define a granular permissions model for room Owner, Moderator, and Co-host roles that scopes abilities to set anonymity modes, adjust message/reaction limits, perform anonymous moderation actions, view audit logs, and manage invites without granting access to real user identities. Provide an intuitive UI for assigning roles, displaying effective permissions, and logging all changes. Ensure permission checks are enforced server-side, exposed through a consistent API, and reflected across all clients. Include least-privilege defaults, exportable audit trails, and safeguards against privilege escalation.

Acceptance Criteria
Role assignment and effective permissions UI
Given I am the room Owner and the room has members A and B When I open Room Settings > Roles Then I can assign 'Moderator' to member A and 'Co-host' to member B And the UI displays the effective permission list for each role before confirming And permissions that I do not have are not shown as assignable And upon confirming, a role-change event is persisted server-side with timestamp, actor role, target role, and diff of permissions, without PII or real identities And all connected clients reflect the updated roles within 2 seconds via real-time sync
Anonymity mode management without identity access
Given I am a Moderator with permission 'ManageAnonymity' in room R When I set anonymity mode to 'Full Ghost' Then the server updates the room configuration and no API endpoint in room R exposes real user identifiers And join screens and room headers display the updated privacy rules to all participants within 2 seconds And the change is recorded in audit logs with actor role, previous/new mode, and timestamp And if I lack 'ManageAnonymity', the server returns 403 and no state change occurs
Room message and reaction rate limits
Given I am a Moderator with 'ManageRateLimits' When I set message limit to 10 per 5 minutes and reaction limit to 30 per 5 minutes Then the server enforces the limits per user and per room starting immediately And users exceeding limits receive a non-identifying rate-limit error with remaining time to reset And the effective limits are visible in Room Info for all participants And the changes are logged in the audit trail; lacking permission returns 403 and no changes occur
Anonymous moderation actions
Given I am a Moderator with 'ModerateMembers' When I mute a ghost participant for 24 hours Then the action succeeds without revealing the participant's real identity to me or in any UI/API response And the muted participant receives a neutral notification with duration and room name only And the audit log records actor role, action, duration, and an anonymized target reference And attempts to reveal identity via clients or API return 403 and log a blocked escalation attempt
Audit log access and export
Given I am the room Owner When I request an audit log export for the last 7 days as CSV Then the server returns a file containing timestamp (UTC ISO-8601), actor role, action type, resource, before/after values, and anonymized identifiers only And entries are ordered and include a monotonic sequence or checksum to detect tampering And only Owners can export; Moderators and Co-hosts receive 403 And the export request itself is recorded in the audit log
Server-side permission enforcement across clients and API
Given I am a Co-host without 'ManageRoles' When I try to promote myself to Owner using the public API or from any client Then the server denies the request with 403 and a stable error code And the attempt is recorded in audit logs including source client type And no UI reflects any change And permission checks are applied even if the client is offline and later syncs
Least-privilege defaults and permission transparency
Given a new room is created When members join, their default role is 'Member' with no administrative permissions Then only the Owner has 'ManageRoles', 'ExportAuditLogs', and 'ManageAnonymity' by default And newly added Moderators have 'ModerateMembers' and 'ManageRateLimits' only by default And newly added Co-hosts have no permissions by default until explicitly granted And each user can view their effective permissions list in the UI and via GET /me/permissions

Shadow Reactions

React with likes, boosts, or emotes that aggregate as counts and transient vibe trails—never tagged to a user. Delivers motivating social feedback for check-ins without spotlighting individuals, reducing anxiety for shy participants and keeping public rooms welcoming.

Requirements

Anonymous Reaction Aggregation
"As a shy participant, I want to react without my identity showing so that I can encourage others without feeling exposed."
Description

Implement a backend service that records Shadow Reactions (likes, boosts, and selected emotes) strictly as aggregated counts per check-in and reaction type, never storing or exposing any identifier linkable to a user. Enforce per-check-in deduplication (e.g., one reaction per type per user session) using unlinkable, rotating client-side tokens and server-side idempotency keys that expire within a defined window. Provide create/list APIs that return only aggregate counts and timestamps, with no user-level data. Ensure counters are resilient to concurrent updates via atomic increments and conflict-free data structures, and persist safely with audit logs that contain no user identifiers. This requirement delivers low-anxiety social feedback aligned with StreakShare’s ethos by guaranteeing non-attribution while keeping counts accurate and tamper-resistant.

Acceptance Criteria
Create Reaction Aggregates Only
Given a valid create-reaction request with check_in_id and reaction_type in {like, boost, emote:<id>} When the server processes the request Then only the aggregate counter for (check_in_id, reaction_type) is incremented by 1 and last_updated_at is set by the server And no user_id, account_id, username, device_id, IP address, or session token is stored in any table or returned in the response And client-supplied timestamps are ignored and not persisted
Aggregate-Only List API Response
Given an existing check_in_id with prior reactions When the client calls GET /reactions?check_in_id=<id> Then the response includes only: check_in_id, an array of {reaction_type, count, last_updated_at}, and a server timestamp And the response contains no user identifiers, token echoes, IP addresses, or per-user reaction details And counts are non-negative integers and last_updated_at is RFC3339 and reflects server time
Per-Check-in Deduplication by Session Token
Given the same client session token and the same (check_in_id, reaction_type) When the client submits multiple create-reaction requests within the configured dedup_ttl Then the aggregate count is incremented at most once And subsequent requests are treated as duplicates (no additional increment) and return a deterministic idempotent response And submitting a different reaction_type for the same check_in_id increments that type independently
Idempotency Keys With Expiry Window
Given two identical create-reaction requests carrying the same Idempotency-Key within idempotency_ttl When both requests reach the server Then the operation is executed at most once and both responses share the same outcome and request_id And after idempotency_ttl has elapsed, resubmitting with the same Idempotency-Key is treated as a new request And Idempotency-Key material is not stored in logs or persistence in reversible form
Concurrent Update Consistency Under Load
Given N concurrent create-reaction requests with unique Idempotency-Keys for the same (check_in_id, reaction_type) When all requests complete Then the aggregate count increases by exactly N with no lost updates or double increments And interleaved requests for different reaction_type values update only their respective counters And the system maintains correctness under at least N=1000 concurrent requests in staging with zero anomalies recorded
Audit Logs Without User Identifiers
Given reaction creation and listing operations occur When audit logs are produced Then each log entry contains only non-attributive fields (e.g., check_in_id, reaction_type, count_delta, server_timestamp, request_id, idempotency_key_digest) And no user identifiers, IP addresses, device identifiers, raw tokens, or hashes reversible to user identity are present And audit logs pass an automated schema check that rejects entries containing prohibited fields
Unlinkable Token Rotation Behavior
Given a client rotates its session token per the configured rotation policy When it submits a create-reaction for the same (check_in_id, reaction_type) after rotation and outside dedup_ttl Then the server cannot correlate the new token to the prior token in storage or logs And raw session tokens are never persisted; any stored reference is a one-way digest with TTL <= dedup_ttl and is purged on expiry And deduplication scope is limited to (check_in_id, reaction_type, session_token) and does not block reactions on other check_ins or reaction_types
Transient Vibe Trails Rendering
"As a room member, I want to see a live, ambient wave of reactions so that I feel the room’s energy without calling anyone out."
Description

Create a client-side visualization layer that transforms incoming aggregated reaction events into short-lived, ambient “vibe trails” (e.g., ripples, glows, or confetti) that decay smoothly over 3–7 seconds without ever indicating who reacted. Trails should scale intensity with volume (burst mapping) and respect performance budgets (60 FPS target, <16 ms frame budget) with graceful degradation on low-end devices. Support theming (light/dark), accessibility (reduced motion settings, color contrast), and localization for any embedded labels. Ensure that trails are room-friendly and non-distracting, with rate-capped particle systems and adaptive decay to prevent visual overload during spikes. No individual avatars, names, or hints of origin may be rendered.

Acceptance Criteria
Aggregate Burst Mapping to Intensity
Given aggregated reaction counts per reaction type are collected in 500 ms windows When a window closes Then spawn at most one vibe trail per reaction type per window and map intensity levels as: 1–2=L1, 3–5=L2, 6–10=L3, ≥11=L4 (clamped) And scale rendering by level as follows: L1=10±2 particles, radius 8–12 px, peak opacity ≤0.40; L2=20±3 particles, radius 12–16 px, peak opacity ≤0.55; L3=35±5 particles, radius 16–20 px, peak opacity ≤0.70; L4=50±5 particles, radius 20–24 px, peak opacity ≤0.85 And do not exceed 50 concurrently active trails; if a new window would exceed the cap, merge the new intensity into the nearest existing trail of the same type within 300 px rather than spawning a new trail
Smooth Decay Timing and Easing
Given any spawned vibe trail When its animation plays Then the animation starts within ≤100 ms of event receipt And total lifetime is: L1=3.0±0.2 s, L2=4.0±0.2 s, L3=5.5±0.2 s, L4=7.0±0.2 s And opacity and scale transitions use an ease-out curve equivalent to cubic-bezier(0.22,0.61,0.36,1.00) with no frame-to-frame opacity delta >0.10 at 60 Hz And trails do not intercept input (pointer-events pass through) and never block UI interactions
Performance Budget and Graceful Degradation
Given a sustained load of 30 aggregated reactions per second for 10 seconds in a public room When running on a mid-tier device profile (e.g., iPhone 11/A13 or Pixel 5/SD765G) Then 95th percentile frame time ≤16 ms, dropped frames ≤1%, and average FPS ≥60 When running on a low-tier device profile (e.g., 2018 Android SD660-class or iPhone SE 1st gen) Then graceful degradation is enabled (≥50% particle reduction, post-processing disabled), 95th percentile frame time ≤33 ms, and average FPS ≥30 And peak additional memory used by the renderer under this load ≤30 MB above idle, with 99th percentile JS main-thread pause ≤20 ms
Theming, Contrast, and Localization
Given the app theme is Light or Dark When rendering vibe trails Then color tokens adapt to theme and any embedded text label achieves WCAG AA contrast ≥4.5:1; non-text critical visuals maintain ≥3:1 perceived contrast And switching themes applies updated tokens within ≤100 ms without flicker artifacts Given a numeric label is displayed with a trail (e.g., +23) When the user locale changes (including RTL locales) Then the label is sourced from i18n keys, respects locale numerals/grouping, mirrors correctly in RTL, and does not overflow its container; if overflow would occur, a localized compact format (e.g., 1.2K) is used
Accessibility: Reduced Motion Behavior
Given system prefers-reduced-motion is true or the in-app Reduce Motion setting is enabled When reactions occur Then particle animations are replaced with a static glow or subtle opacity pulse with no movement, completing in ≤800 ms And flashes do not exceed 3 per second and luminance change per flash ≤25% And the setting persists across sessions and platforms for the user
Anonymity and Originless Rendering
Given any vibe trail is rendered When users observe, hover, or tap Then no avatars, names, initials, profile colors, or unique identifiers are displayed, and no tooltips reveal origin And the UI/DOM/Canvas contains no data attributes or debug strings that include user IDs or hashes for trails (aggregate-only metadata allowed) And any optional label contains only aggregate counts and generic reaction type, with no per-user hints like “from followers”
Rate Capping and Adaptive Decay During Spikes
Given a spike of reactions exceeding 20 reactions per second When rendering trails Then spawn rate is capped at ≤20 new trails/second across all reaction types, with excess reactions merged into existing trails by increasing intensity up to L4 And concurrently active trails are capped at ≤50; if the cap would be exceeded, new spawns are deferred or merged while preserving the 3.0 s minimum lifetime And during a synthetic spike of 100 reactions/s for 10 s, FPS never drops below 30, active trails never exceed 50, and no trail lifetime falls below 3.0 s
One-Tap Reaction UX & Undo
"As a busy remote worker, I want to add a reaction with one tap and undo if I mis-tap so that giving feedback is effortless and low-risk."
Description

Design a minimal, high-speed reaction UI on each check-in that supports one-tap “Like,” quick access to “Boost,” and a long-press palette for emotes. Use optimistic UI updates with subtle haptic and micro-animations, followed by server confirmation; provide a 3-second undo snackbar to retract accidental taps, with server-side cancellation if not already aggregated. Include offline-first behavior (queue and replay on reconnect), clear error states, and keyboard/screen-reader accessibility. Ensure the control is reachable within one thumb zone on mobile and does not expose any actor identity in the UI or post histories. Keep the interaction sub-300 ms end-to-end on a healthy connection.

Acceptance Criteria
One-Tap Like: Optimistic, Haptic, and 3-Second Undo
Given a user views a check-in on mobile with a healthy connection (<=100 ms RTT) When the user taps the Like control once Then the Like count increments optimistically within 100 ms And a micro-animation plays and light haptic feedback triggers within 150 ms And an Undo snackbar appears for 3 seconds with a focusable action And server confirmation is received and reflected within 300 ms of the tap And no user identity is displayed anywhere in the reaction UI If the user taps Undo within 3 seconds and the server has not aggregated the reaction Then the reaction is cancelled server-side and the count returns to its prior value within 150 ms If the user taps Undo after the reaction has been aggregated server-side Then the UI restores the aggregated count and shows "Undo unavailable" inline for 2 seconds with an accessibility announcement If the server responds with an error before confirmation Then the optimistic count is reverted within 100 ms and an inline error "Couldn't react. Try again." is shown and announced
Quick Boost Access and Confirmation
Given the reaction affordance is visible on a check-in When the user reveals Boost via a single additional action and taps Boost Then Boost becomes selected and the Boost count increments optimistically within 100 ms And server confirmation arrives and is reflected within 300 ms of the Boost tap And an Undo snackbar appears for 3 seconds allowing cancellation with the same server-side rules as Likes And no more than 1 additional tap is required from rest state to activate Boost And no actor identity is displayed in any Boost UI element or history
Long-Press Emote Palette with Full Accessibility
Given the user long-presses (>=500 ms) the reaction affordance on mobile When the emote palette opens Then it appears within 150 ms and shows at least 6 emotes with accessible labels and no identities And selecting an emote applies an optimistic increment within 100 ms with a micro-animation and shows a 3-second Undo And server confirmation is reflected within 300 ms And keyboard users can open the palette with Enter or Space, navigate with Arrow keys, select with Enter, and dismiss with Esc And screen readers announce control names, counts, selection state, and Undo via a live region, with all reaction targets >= 44x44 dp and visible focus indicators
Offline Queue and Replay for Reactions
Given the device is offline when the user reacts (Like, Boost, or Emote) When the user taps the reaction Then the UI applies the optimistic change, marks it as "Queued" visually, and shows a 3-second Undo And the reaction is added to a local queue with timestamp and type When connectivity is restored Then queued reactions are sent FIFO and confirmed; on success, the "Queued" state clears and counts remain If any queued reaction fails server validation Then its optimistic change is reverted within 100 ms and an inline error with a Retry action is shown and announced
Error States and Undo Cancellation Rules
Given a reaction request times out (>5 s) or receives a 4xx/5xx error When the error occurs Then the optimistic change is reverted within 100 ms, an inline error with Retry appears for 10 seconds, and an accessibility announcement is made If the user taps Undo within 3 seconds after an error Then no network request is made and the UI remains reverted If the user taps Retry Then the request is re-issued and on success the count updates and error clears; on failure, the error persists without exposing any user identity
Anonymity and Aggregation Only
Given any reaction is added or undone When the UI updates counts and trails Then only aggregate counts and transient visuals are displayed And no usernames, avatars, initials, or identifiable markers are shown in tooltips, tiles, palettes, snackbars, or histories And the client never renders a list of reacting users, and exported or shareable views contain only aggregate counts
Performance and Thumb-Zone Reachability
Given a healthy connection When the user performs any reaction action (Like, Boost, select Emote) Then time from tap or selection to server-confirmed state displayed is <= 300 ms p95 And optimistic visual feedback begins within 100 ms p95 On phones in portrait between 360x780 dp and 430x932 dp Then the one-tap Like control's center lies within the lower 40% of screen height and within 100 dp of the nearest horizontal edge And all reaction targets are at least 44x44 dp with 8 dp spacing and are reachable with one thumb without scrolling
Real-Time Reaction Broadcast & Sync
"As a participant in a live room, I want reaction counts to update instantly even on flaky networks so that the experience feels alive and reliable."
Description

Enable live reaction updates via WebSockets or HTTP/2 server-sent events with room-scoped channels. Implement batched fan-out and delta payloads to minimize bandwidth while keeping perceived latency under 250 ms p95. Provide retry, exponential backoff, and offline buffering with eventual consistency upon reconnection. Ensure idempotent processing of late or duplicated packets and shard counters for scale. Define SLAs (99.9% availability for reaction transport), observability (metrics, logs without user identifiers, and distributed tracing), and back-pressure strategies to prevent client overload during spikes. Clients should reconcile server truth on reconnect, resolving any optimistic UI discrepancies gracefully.

Acceptance Criteria
P95 Real-Time Reaction Latency Under 250 ms
Given a user reacts in a room with 500–1,000 concurrent subscribers under nominal load When the client sends a reaction event over WebSocket/SSE Then the end-to-end time from client send to client UI update is <= 250 ms at p95 and measured via synchronized client/server clocks over a rolling 5-minute window And p50 <= 120 ms; no more than 1% message drops without retry within the window And latency instrumentation is enabled by default and exportable per room and region
Room-Scoped Broadcast Isolation
Given two public rooms A and B with overlapping audiences When reactions are sent in room A Then only clients subscribed to room A receive reaction updates; clients subscribed only to room B receive none And reaction messages include a roomId matching the subscribed channel; no cross-room messages appear in network logs or client event streams And subscribing/unsubscribing to rooms immediately updates delivery such that new messages respect the latest subscription set
Delta Payloads and Batched Fan-Out Efficiency
Given a room receiving sustained 100 reactions/second When broadcasting updates to subscribers Then the server emits delta payloads that contain only changed counters since the last acknowledged sequence, including a monotonic sequenceId And fan-out batches with a maximum 50 ms window or 100 events per batch (whichever occurs first) while still meeting the 250 ms p95 latency criterion And subscribers receive a full snapshot only on initial subscribe or recovery; all subsequent updates are deltas And average payload size per update is reduced by >= 60% versus full snapshots in the test scenario
Back-Pressure and Client Overload Protection
Given an inbound reaction spike of 10x the baseline rate for 60 seconds When server outbound queue depth exceeds the configured threshold Then the server sends a throttle instruction to all publishers in the room within 100 ms And clients apply jittered exponential backoff and cap publish rate to the advertised limit until a resume signal is received And clients coalesce local UI reactions to avoid frame drops; UI main-thread time for rendering reaction updates stays < 8 ms/frame on reference devices And fewer than 1% of clients disconnect due to overload; transport recovers to baseline within 60 seconds after the spike
Offline Buffering and Eventual Consistency on Reconnect
Given a client loses connectivity for up to 30 minutes while the user triggers reactions When connectivity is restored Then the client retries buffered reactions with exponential backoff (initial 500 ms, max 30 s, full jitter) and per-event acknowledgments And no more than 0 reactions are lost for up to 1,000 buffered events or 5 MB of storage, whichever limit is reached first; beyond limits, oldest events are dropped and recorded in metrics And upon reconnect the client requests the authoritative snapshot, reconciles within 2 seconds, and corrects any optimistic UI counts without duplicate animations
Idempotent Handling of Duplicates and Late Packets
Given duplicate transmission of the same reaction eventId or arrival out of order When the server processes these events across sharded counters Then duplicates are ignored without incrementing counts; processing remains idempotent And late events older than 10 minutes relative to their client timestamp are discarded with idempotent acknowledgment and recorded in metrics And counter shards update atomically and converge; on a single client connection, read-your-writes consistency is observed within 500 ms
Observability, Privacy, and SLA Compliance
Given production traffic across regions When collecting and inspecting telemetry Then metrics expose transport availability SLI, fan-out success rate, retry counts, queue depth, and p50/p95/p99 latency segmented by room size and region And logs and traces contain no user identifiers or personal data; reaction payloads exclude any userId fields to preserve shadow reactions And distributed tracing links client, gateway, broadcaster, and subscriber spans under a single traceId for a reaction flow And monthly transport availability is >= 99.9% with health checks and alerting on error budget burn at 25%, 50%, and 100% thresholds
Privacy Thresholds & Anti-Fingerprinting
"As a privacy-conscious user, I want safeguards that prevent others from inferring my actions so that I can participate confidently."
Description

Apply privacy-preserving display rules so individual actions cannot be inferred: suppress or bucket reaction increments in small rooms until a minimum k threshold (e.g., k≥3) is met, add slight time and magnitude jitter to visible count changes, and avoid displaying exact timestamps for single events. Do not log IPs or device identifiers alongside reaction events; rotate any anti-abuse tokens frequently and keep them unlinkable across rooms. Define strict data retention (e.g., aggregated counts kept; raw transport metadata discarded within 24 hours) and publish a transparent in-app explanation of Shadow Reactions privacy. Validate compliance with GDPR/CCPA by ensuring no personal data is processed for reactions and supporting data-map documentation.

Acceptance Criteria
k-Threshold Suppression & Bucketing in Small Rooms
Given a room has fewer than 3 unique reactors since the last visible update When 1 or 2 reaction events are received Then the visible reaction count remains unchanged in the UI Given a room reaches 3 unique reactors since the last visible update When the next UI update cycle occurs (≤2 seconds) Then the reaction count increases by the aggregated number of suppressed events without showing per-event increments
Batched Release and Magnitude Jitter with Accurate Final Totals
Given the k-threshold has been met for a room When releasing a visible count update Then the displayed counter increments in randomized step sizes of 1–3 until reaching the true total Given a visible count update has started When 10 seconds have elapsed Then the displayed total equals the true aggregated count for that batch
Time Jitter and Timestamp Hygiene
Given any visible reaction count update When it is scheduled for display Then a random delay between 2 and 10 seconds is applied before the first increment is shown Given a visible reaction update represents fewer than 3 underlying events When a user inspects the time of the update Then no timestamp with precision finer than 15 minutes is displayed (use a bucketed label such as "recent") Given a visible reaction update represents 3 or more events When a user inspects the time of the update Then the time is rounded to the nearest 5 minutes and never includes seconds
No IPs or Device Identifiers Persisted with Reaction Events
Given a reaction event is processed server-side When data is written to any persistent store (DB, analytics, logs) Then no IP address, user-agent string, device identifier, or fingerprint is stored alongside the event Given production observability is queried for the last 24 hours When searching reaction ingestion and processing pipelines Then zero records contain IP addresses or device identifiers Given schema migrations are inspected When checking reaction event tables Then there are no columns for IP, device ID, or user agent
Rotating, Unlinkable Anti-Abuse Tokens
Given a client reacts in Room A and then in Room B within a 24-hour window When comparing anti-abuse tokens used for those reactions Then the tokens differ and cannot be deterministically correlated Given 24 hours elapse in the same room When the client reacts again Then a new token is used and the previous token is rejected for further writes Given logs over 48 hours across multiple rooms When scanning token values Then no token string appears in more than one room or beyond its rotation window
Raw Metadata Retention ≤24h; Aggregated Counts Retained
Given raw transport metadata (e.g., IP, user agent, request IDs) related to reactions is captured transiently at the edge When 25 hours have elapsed since capture Then zero rows of such raw metadata remain in any persistent store Given historical reaction data older than 24 hours is queried When results are returned Then only aggregated counts are retrievable and no individual event-level data is available Given the deletion job runs daily When it fails to reduce raw rows to zero by 25 hours Then an alert is emitted to on-call within 5 minutes
Transparency Notice and GDPR/CCPA Data Map
Given a user is on the Shadow Reactions UI When they open the in-app "Privacy of reactions" page Then the page loads within 2 seconds and clearly states: k-threshold suppression, time/magnitude jitter, absence of IP/device identifiers, token rotation cadence, and ≤24h raw metadata retention, with a date-stamped version Given a GDPR/CCPA compliance review is performed When the data map is examined Then reaction events are documented as non-personal data, flows are enumerated, and no personal data fields are processed for reactions Given a data subject request cites reaction data When the request is fulfilled Then the response states no personal data is stored for reactions and includes a link to the in-app privacy explanation
Moderation & Rate Limiting Controls
"As a room host, I want to configure reaction behavior and prevent spam so that rooms stay welcoming and on-task."
Description

Provide room-level controls for hosts to enable/disable Shadow Reactions, restrict specific emotes, and set per-user reaction rate limits (e.g., max N reactions per minute per room). Implement server-side flood protection and anomaly detection (burst caps, cooldowns) while preserving anonymity. Add an aggregated reactions insights panel for hosts that shows volume trends without exposing any individual attribution. Include abuse-reporting flows for emote packs and a global blocklist for inappropriate assets. All controls must integrate with existing room permissions and audit logs without storing user identities for reaction events.

Acceptance Criteria
Room-Level Toggle for Shadow Reactions
- Given I am a host or co-host with Manage Room permissions, when I toggle Shadow Reactions off for a room, then non-host clients no longer see reaction controls within 5 seconds and the server rejects new reaction events with error_code="REACTIONS_DISABLED". - Given Shadow Reactions are off, when any client submits a reaction, then the reaction is not counted, not broadcast, and a non-blocking notice informs the sender that reactions are disabled. - Given I toggle Shadow Reactions on, when the change is saved, then new reactions are accepted and reflected in counts and vibe trails within 2 seconds end-to-end. - Given any enable/disable change, then an audit log entry records timestamp, actor role, room ID, action, and before/after values, and no reaction event is stored with any user identifier. - Given a viewer lacks the required permission, when they open room settings, then Shadow Reactions controls are not visible or are read-only.
Restrict Specific Emotes per Room
- Given I am a host in Emote Controls, when I restrict one or more emotes for this room, then restricted emotes disappear from the picker for all participants within 5 seconds. - Given an emote is restricted, when any client attempts to send it, then the server rejects it with error_code="EMOTE_BLOCKED", the count does not increment, and no vibe trail renders. - Given an emote was restricted, when I unrestrict it, then it reappears in the picker and is usable within 5 seconds. - Given emotes are restricted or unrestricted, then an audit log entry captures the emote IDs added/removed and actor role, with no user attribution for any reaction event. - Given restricted emotes exist, when I view insights, then historical counts remain visible and are labeled "blocked" from the restriction timestamp forward, without any per-user details.
Per-User Reaction Rate Limits per Room
- Given a room rate limit of N reactions per user per minute is configured, when a user submits more than N reactions in any rolling 60-second window, then each excess reaction is rejected with error_code="RATE_LIMITED" and a retry_after value in seconds. - Given the same user posts reactions in a different room, then the rate limit is evaluated independently per room. - Given I change N, when I save the new limit, then the new limit takes effect within 10 seconds for all participants. - Given rate limiting is enforced, then enforcement occurs server-side and cannot be bypassed by client version, and no persisted reaction event contains any user identifier; audit logs include only configuration changes. - Given connectivity changes (e.g., reconnects), when the same user continues reacting, then the rolling window enforcement persists for the user without storing user identifiers with reaction events.
Server-Side Flood Protection and Cooldowns
- Given burst protection is enabled with a burst cap B over 5 seconds and cooldown C seconds, when any single source attempts more than B reactions within 5 seconds, then subsequent reactions during the next C seconds are rejected with error_code="COOLDOWN_ACTIVE" and remaining cooldown seconds are returned. - Given an anomalous room-wide spike occurs where reactions per minute exceed 300% of the 7-day median for that minute, then an anomaly flag is raised and visible to hosts in insights within 30 seconds. - Given flood protection or anomalies are triggered, then no user identity is logged or exposed; audit logs record only room-level events and thresholds crossed. - Given the cooldown expires, when the same source resumes sending reactions within policy, then reactions are accepted and counted normally.
Aggregated Reaction Insights Panel (No Attribution)
- Given I am a host or co-host with analytics permission, when I open the reactions insights panel, then I see aggregated reaction counts by time bucket and by emote type for a selectable range (15m, 1h, 24h, 7d) with a maximum 10-second data latency. - Given I view insights, then no UI element or export provides per-user or per-session attribution, and the API responses contain no user-identifying fields for reaction events. - Given Shadow Reactions are disabled at time T, when I view the timeline, then no new reactions are counted after T, and the disable/enable events are annotated. - Given rate limits or flood protection were active, then the panel indicates periods of limiting as overlays without exposing any individual actors.
Abuse Reporting and Global Emote Blocklist
- Given a participant views the emote picker, when they report an emote pack for abuse selecting a reason and optional comment, then a report is submitted and an acknowledgement is shown within 3 seconds. - Given a moderator adds a pack to the global blocklist, then within 60 seconds the pack's assets no longer appear in any room and attempts to use them are rejected with error_code="ASSET_BLOCKED"; previously rendered assets are replaced with a placeholder. - Given a pack is unblocked, then its assets become available again globally within 60 seconds. - Given any blocklist or un/block action occurs, then an audit log captures actor role, pack ID, action, and timestamp; reaction events remain anonymous with no user identifiers stored. - Given a room has locally restricted emotes and a pack is globally blocked, then the stricter rule applies and no blocked asset can be used.

Raise-to-Check

Start and finish a check-in from your watch with a single wrist-raise and tap. Haptic-only confirmation keeps things discreet during meetings, letting you save a streak in under two seconds without unlocking your phone or opening the app.

Requirements

Raise Gesture + One-Tap Check-in
"As a remote professional, I want to raise my wrist and confirm a check-in with one tap so that I can save my streak in under two seconds without using my phone."
Description

Implements a watch-based flow that surfaces a lightweight check-in overlay immediately upon wrist raise and screen wake, enabling a single tap to confirm a habit check-in. The overlay should appear via a complication, Smart Stack card, or quick-launch mechanism to comply with wearable OS constraints, with sub-2s interaction from wake to confirmation. Includes debounce to prevent accidental double submissions, minimal UI footprint, and energy-efficient execution. Integrates with the mobile app and backend check-in API to create a check-in event, update streak counters, and trigger relevant analytics. Must handle multiple open habits by presenting a minimal selector when necessary while still enabling a one-tap default path.

Acceptance Criteria
Wake-to-Overlay within 2 Seconds
Given the user has a supported entry point configured (complication, Smart Stack card, or quick-launch) When the user raises their wrist and the screen wakes Then the StreakShare check-in overlay is reachable with a single tap from the wake screen Given the user taps the supported entry When the overlay is requested Then the overlay renders and is interactive within 500 ms Given the overlay is visible with one open habit When the user performs the single confirmation tap Then the total elapsed time from screen wake to haptic confirmation is <= 2.0 seconds at the 90th percentile across 20 trials
One-Tap Confirmation with Discreet Haptics
Given the overlay shows a single open habit When the user taps Confirm Then exactly one haptic success pattern is played and no audible sound is emitted regardless of system sound state Given confirmation succeeds When the haptic plays Then the overlay auto-dismisses and returns to the watch face within 1 second, and no additional confirmation dialog is shown Given the phone is locked or not present When the user confirms on the watch Then the check-in completes without requiring phone unlock or app foregrounding
Debounce and Idempotent Submission
Given the user taps Confirm When any additional taps occur within 2 seconds or before the first request resolves Then those taps are ignored and no duplicate submissions are created Given intermittent connectivity When the same confirmation is retried by the client Then a consistent clientRequestId ensures the backend creates at most one check-in record Given a duplicate or already-checked-in server response When it is received Then the UI shows a single success haptic and state reflects exactly one check-in
Multi-Habit Minimal Selector with One-Tap Default
Given multiple open habits exist When the overlay appears Then the highest-priority habit is preselected and a single tap confirms that default Given the user wishes to change the target habit When they switch selection Then it requires at most one additional tap to change and one tap to confirm (maximum of two taps total) Given the multi-habit overlay When it is rendered Then it fits without scrolling, presents no more than two actionable controls initially, and occupies no more than 50% of the vertical screen height
Backend Integration, Streak Update, and Analytics
Given a successful confirmation When the request is sent Then the watch posts a check-in event to the backend with userId, habitId, UTC timestamp, device=watch, and source=raise_to_check, and receives a 2xx response Given the backend acknowledges success When the response is processed Then the habit’s streak counter is incremented locally and the watch complication/card reflects the new streak within 2 seconds Given the phone app is paired When a check-in is completed on the watch Then the phone reflects the new streak within 10 seconds via background sync or immediately on next app open Given analytics are enabled When the user confirms Then an analytics event checkin_confirmed is emitted with properties including latency_ms and result (success or queued)
Power Efficiency and Minimal Runtime
Given a standard check-in flow When executed Then the overlay shows only habit name, streak count, primary confirm, and optional habit switcher, with at most two tappable controls initially and no looping animations Given the confirmation completes or queues When the UI updates Then the app returns to the watch face and relinquishes foreground within 1 second and does not keep a background task alive longer than 2 seconds Given instrumentation via watchOS energy logs When 20 consecutive check-ins are performed Then average network payload per check-in is <= 6 KB and CPU active time attributable to StreakShare is <= 300 ms per check-in
Focus/Silent Mode Discretion
Given the watch is in Silent, Do Not Disturb, or a Work Focus When the user confirms a check-in Then only haptic feedback is produced and no audible sound or phone notification banner is generated Given Theater Mode is enabled When the user manually wakes the screen and triggers the overlay Then the overlay behaves identically and still provides haptic-only confirmation without overriding Theater Mode Given system haptic strength settings When confirmation occurs Then the haptic respects the user’s configured intensity
Discreet Haptic-Only Confirmation
"As a meeting attendee, I want discreet haptic-only confirmation so that I can check in without drawing attention."
Description

Delivers silent, haptic-only feedback on check-in success or failure to preserve discretion during meetings and quiet environments. Uses distinct haptic patterns for success, failure, and retry prompts, auto-respects system mute settings, and offers a user-toggleable "Meeting Mode" that suppresses on-screen visuals beyond a minimal confirmation tick. Includes feedback for edge cases such as connectivity loss or duplicate checks and a subtle retry affordance without requiring phone interaction.

Acceptance Criteria
Meeting Mode: Successful Haptic-Only Confirmation
Given Meeting Mode is enabled and the device is on system mute or Do Not Disturb, When the user raises their wrist and taps to check in with stable connectivity, Then the watch emits the Success haptic pattern (S) within 200 ms of server acknowledgement and no later than 2.0 s after tap, And no audible sound is played, And the screen stays off or shows a dim tick icon for ≤ 1.0 s with no text, And exactly one check-in record is created for the habit.
Meeting Mode: Connectivity Loss Failure and Discreet Retry
Given Meeting Mode is enabled and network connectivity is unavailable at tap time, When the user raises their wrist and taps to check in, Then the Failure haptic pattern (F) is emitted within 1.0 s of tap, And a Retry prompt haptic (R) is emitted 2.0 s later if still offline, And a second tap within 10 s initiates one retry attempt, And no on-screen text or alerts are displayed, And zero check-in records are created unless a retry succeeds (then emit S).
Duplicate Check-In Attempt Haptic
Given the user has already checked in for the habit during the current streak window, When the user attempts a check-in via Raise-to-Check, Then the Duplicate haptic pattern (D) is emitted within 1.0 s, And no new record is created, And if local state detects duplication, no network request is sent; otherwise, on a 409/duplicate response the D pattern is emitted.
System Mute and Sound Suppression
Given system mute or Focus/Do Not Disturb is enabled or disabled, When a check-in succeeds or fails via Raise-to-Check, Then the app emits haptics only (no audio) in all cases, And it never overrides system mute to play sound, And screen wake does not exceed the platform’s minimum wake for a notification; in Meeting Mode the screen remains off or displays a dim tick for ≤ 1.0 s.
Distinct Haptic Patterns Are Learnable and Discriminable
Rule: Success (S) = 2 short pulses (120 ms each, 80 ms gap), total ≤ 320 ms. Rule: Failure (F) = 1 long pulse (400 ms) + 1 short (120 ms), 120 ms gap. Rule: Retry prompt (R) = 1 short pulse (120 ms). Rule: Duplicate (D) = 3 very short pulses (80 ms each, 60 ms gaps). Rule: In a validation test with n ≥ 20 target users after a 30-second training, ≥ 90% correctly identify S, F, R, D by feel alone. Rule: Across supported watch models, pulse durations vary by ≤ 20 ms and remain distinguishable per the validation test.
Subtle On-Wrist Retry Without Phone Interaction
Given a Failure (F) was emitted within the last 10 s, When the user performs one additional tap within that 10 s window while Meeting Mode is enabled, Then a single retry request is sent, And the watch emits the Retry prompt haptic (R) immediately and then either S or F within 2.0 s of completion, And no screen text or alerts are shown, And no more than one retry is accepted per failure event.
Offline Check-in with Deferred Sync
"As a traveler with spotty connectivity, I want my check-ins to work offline and sync later so that my streaks are preserved even without internet."
Description

Enables check-ins to be recorded on the watch without network connectivity by storing events locally with precise timestamps and idempotency keys. On reconnection, queued events are securely synced to the paired phone or directly to the backend. Implements conflict resolution (e.g., idempotent submission per habit/day and grace-window last-write-wins) to prevent duplicates and maintain streak integrity. Provides subtle on-watch indicators of pending sync and auto-retry backoff, ensuring streaks are preserved regardless of connectivity.

Acceptance Criteria
Offline wrist-raise check-in capture
Given the watch has no network connectivity and the paired phone is unreachable And the user is authenticated with at least one active habit enabled for Raise-to-Check When the user raises their wrist and taps to check in Then a local check-in event is persisted to durable storage within 500 ms of the tap And the event includes fields: habitId, idempotencyKey, tapTimestamp (ISO 8601 with milliseconds), timezoneId, timezoneOffsetMinutes, deviceId, monotonicSequence And a single discreet haptic confirmation is delivered within 500 ms of the tap with no audible output And no network request is attempted while offline And the queued event remains after app or device reboot
Deferred sync upon reconnection with idempotency
Given one or more queued offline check-in events exist on the watch And connectivity is restored either via the paired phone or direct network on the watch When sync is triggered automatically Then all queued events begin transmission within 5 seconds And each event is submitted with its idempotencyKey so that duplicate submissions for the same habit/day result in a single backend record And on a 2xx or idempotent-duplicate response, the event is marked Synced and removed from the queue And the backend reflects exactly one check-in per habit/day for the submitted keys And the watch UI reflects 0 pending items within 2 seconds after successful sync
Grace-window conflict resolution (last-write-wins)
Given two check-in events exist for the same habit and dayKey (computed from the user's local day boundary) And their tapTimestamp values differ When both events are processed by the backend Then if the timestamps are within a configurable grace window (default 5 minutes), the later tapTimestamp supersedes the earlier and becomes the stored check-in And if the timestamps are outside the grace window, the first successfully stored event is retained and subsequent events are treated as duplicates And exactly one check-in is stored for that habit/dayKey And the client marks superseded or duplicate events as Resolved and removes them from the queue
Pending sync indicators (discreet)
Given at least one unsynced check-in event is queued on the watch When the user views the Raise-to-Check tile or complication Then a subtle pending indicator is displayed within 500 ms showing the count of queued events And no audible alert is produced at any time And the indicator count decrements as events successfully sync and reaches zero when the queue is empty And the indicator is removed within 1 second of the queue reaching zero
Auto-retry with exponential backoff and persistence
Given a queued event fails to sync due to transient errors (network timeout, 5xx) When the client schedules retries Then retries follow exponential backoff starting at 5 seconds and doubling up to a maximum interval of 5 minutes with ±10% jitter And retries continue automatically for at least 24 hours or until success And the retry schedule and queue survive app and device restarts, resuming within 10 seconds of relaunch And on receiving an idempotent-duplicate or other 2xx indicating the record already exists, the client marks the event Synced and stops retrying And on receiving unrecoverable 4xx (excluding idempotent duplicate), the client marks the event Failed (with reason) and continues processing other queued events
Secure multi-path sync (phone relay and direct backend)
Given the watch has queued events and at least one sync path is available When the paired phone is connected and eligible for relay Then events are sent to the phone over an encrypted OS transport and forwarded to the backend over TLS 1.2+ within 5 seconds And when the phone relay is unavailable but the watch has Wi‑Fi/LTE, events are sent directly to the backend over TLS 1.2+ with a valid OAuth token And for any idempotencyKey, only one path is used per attempt to prevent duplicate transmissions And queued events are encrypted at rest using platform-secure storage And no PII beyond habitId and hashed idempotencyKey appears in logs
Time zone, DST, and clock drift correctness
Given the user checks in near a local day boundary, during a DST transition, or after changing time zones When the idempotencyKey and dayKey are computed Then dayKey is derived from the user's Olson time zone at tap time (not UTC) and aligns with the habit's local day boundary And the backend stores the original tapTimestamp and attributes the check-in to the correct dayKey regardless of subsequent time zone changes And device clock drift up to ±5 minutes relative to server time does not break streak integrity; ordering within the grace window is resolved using tapTimestamp, then server receipt time as a tiebreaker And streak continuity is preserved across time zone changes and DST shifts with no double-counts or missed days
Auto Habit Selection and Quick Switch
"As a power user with multiple habits, I want the watch to auto-select the most likely habit and let me switch quickly so that I can check in with minimal friction."
Description

Automatically preselects the most relevant habit for Raise-to-Check using signals such as schedule, recent activity, location context, and active micro-commitment rooms. When multiple candidates exist, presents a minimalist carousel or swipe-to-cycle interaction before confirmation, preserving the one-tap happy path when a clear default exists. Includes user preferences to pin favorites, set time windows, and override auto-selection logic. Reduces cognitive load and interaction steps, keeping the total flow under two seconds for common cases.

Acceptance Criteria
One-Tap Default When Clear Habit Exists
Given a single habit matches the current time window or is marked as Always Default for this window and there is no active overlapping room for another habit When the user raises the watch and taps confirm Then that habit is preselected, no carousel is shown, confirmation requires one tap, and the check-in is saved with a success haptic And no phone unlock or app open is required
Quick Switch Carousel for Multiple Candidates
Given two or more candidate habits are in the same priority tier for the current context When the Raise-to-Check UI appears Then a minimalist carousel is displayed with the top-ranked habit preselected And swiping left or right changes the selection within 300 ms per swipe And a single confirm tap saves the currently displayed habit And no accidental confirmation occurs during swipe gestures
Respect User Pins and Time Windows
Given the user has pinned favorites and configured time windows When the current time falls within a window for one pinned habit Then that pinned habit is chosen as default with no carousel When the current time falls within overlapping windows for multiple pinned habits of equal priority Then those habits appear in the carousel with the highest-priority pin first When the user edits pins or time windows Then the next Raise-to-Check reflects the change immediately without requiring an app restart
Active Room Priority Overrides Others
Given the user has an active micro-commitment room currently live for a specific habit When Raise-to-Check is invoked during that room’s active window Then that room’s habit is preselected regardless of location, recent activity, or schedule And if multiple active rooms overlap, each appears once in the carousel with the most recently engaged room first And upon confirmation, the check-in is attributed to the active room
Performance: Sub-2-Second Flow
Given typical device conditions (watch awake, app extension active, network available or offline cache ready) When the user performs Raise-to-Check with a clear default Then the time from wrist-raise detection to success haptic is ≤ 2.0 seconds at the 90th percentile and ≤ 1.2 seconds at the median across 30 consecutive attempts And no single step (preselect, render, confirm) exceeds 500 ms And if network latency exceeds 800 ms, the check-in is queued offline and the success haptic still occurs within the ≤ 2.0-second budget
Offline and Low-Signal Fallback Behavior
Given there is no network connectivity or context signals are unavailable (no schedule match, no location, no active room) When Raise-to-Check is invoked Then the last checked-in habit within the past 48 hours is preselected as default And if multiple viable candidates exist (recent or pinned), the carousel is shown And confirmed check-ins are queued and auto-synced when connectivity returns without user intervention And the user receives a success haptic at confirm time indicating a queued state
Real-time Room Update and Streak Safeguards
"As a member of a live room, I want my check-in to update the room and protect my streak within the grace window so that my commitment is visible and my streak doesn’t decay."
Description

After a successful check-in, instantly updates the user’s streaks and broadcasts the event to active rooms via push/WebSocket, refreshing leaderboards, in-session indicators, and reactions in near real-time. Applies grace-window logic to prevent streak decay when check-ins occur near boundaries, and emits a specific "streak saved" signal that maps to a distinct haptic pattern. Ensures sub-1s end-to-end propagation when online, with fallback queuing when offline to maintain consistency across devices and participants.

Acceptance Criteria
Online Sub-1s Propagation to Active Rooms
Given a user with an active streak is online (Wi‑Fi or LTE, round-trip latency ≤150 ms) and at least one active room has ≥1 other participant connected via WebSocket or push When the user completes a check-in via Raise-to-Check and the server acknowledges the check-in Then the check-in event is delivered to all connected room participants within 1.0 seconds of the user tap (T0) with delivery success ≥99% over 100 consecutive test runs And the room leaderboard increments the user’s count by 1 within 1.0 seconds on all connected clients And the user’s in-session indicator flips to "Checked in" within 1.0 seconds on all connected clients And the event includes a unique, idempotent event_id and is not delivered more than once to any client
Grace Window Streak Safeguard at Daily Boundary
Given the app uses the user’s local timezone with a daily cutoff at 00:00 and a grace window of 5 minutes before and after the cutoff When a check-in occurs within 23:55:00–23:59:59 or 00:00:00–00:04:59 local time Then the system assigns the check-in to the calendar day that preserves the existing streak (post-cutoff checks within the window count toward the prior day if that day is otherwise missing) And only one check-in per calendar day is counted toward the streak, with any additional check-ins in the grace window not incrementing the streak And the UI records the assigned_day and shows "Streak saved" for qualifying cases And audit logs persist the original event time (T0), assigned_day, and applied rule, and automated tests confirm no false streak decays across 1,000 randomized boundary times
Distinct Haptic Confirmation for Streak Saved
Given a check-in results in a streak being created, maintained, or rescued (including via grace rules) and the watch is paired and not in Do Not Disturb When the server confirms the streak state change Then the watch plays HAPTIC_STREAK_SAVED_V1 (2 short 100 ms pulses separated by 100 ms, then 1 long 300 ms pulse; total 600 ms; medium intensity) with no audible sound And no other event in the app uses HAPTIC_STREAK_SAVED_V1 And if the check-in is queued offline and not yet confirmed, the watch does not play HAPTIC_STREAK_SAVED_V1 until confirmation; instead it plays HAPTIC_QUEUED_V1 (single 150 ms pulse) one time And automated instrumentation verifies the correct pattern ID is emitted in 95%+ of 100 trials per device model, with timing tolerances ±10 ms
Offline Queueing and Reliable Replay
Given the watch or phone is offline at check-in time and local storage is available When the user completes a check-in via Raise-to-Check Then the event is stored durably with a client-generated idempotency_key, original timestamp (T0), and user timezone snapshot and survives OS kill and reboot And upon connectivity restoration within 24 hours, the client automatically syncs the event, preserving T0 for streak/grace evaluation and emits the check-in to rooms And the replay guarantees exactly-once processing server-side and no duplicate leaderboard increments across retries And if connectivity is not restored within 24 hours, the user is notified on next app open and the event is not applied to past days
Cross-Device Consistency After Check-In
Given the same user is signed in on watch, phone, and web with at least one active room session When a check-in is completed on the watch Then all devices display the same streak count, last check-in timestamp, and assigned_day within 1 second when online, or within 5 seconds of sync when coming back online And the room leaderboard values match across devices and participants for the event_id And if two check-ins are initiated within 2 seconds by the same user from different devices, only one increments the streak and the other returns an "already checked in" response without side effects
Reactions and In-Session Indicators Refresh on Broadcast
Given a room has reactions enabled and multiple participants connected When a check-in event is broadcast Then the "React" affordance appears for that event on all connected clients within 1 second and reaction counts update across participants within 1 second of any reaction being sent And the checked-in badge/indicator appears next to the user within 1 second and resets at the next daily cutoff And no reaction is orphaned; every reaction references a valid event_id and passes schema validation
Secure Watch–Phone Pairing and Authorization
"As a security-conscious user, I want my watch to be securely paired and authorized so that quick check-ins don’t compromise my account."
Description

Establishes a secure pairing and authorization flow between the watch app and the user’s account using device-bound tokens stored in secure enclaves/keychains. Limits token scope to read habits and create check-ins, supports revocation on unpairing, and enforces rate limiting and anomaly detection to prevent abuse. Includes mutual device attestation where available and audits all check-in events with device metadata for traceability. Ensures quick interactions do not compromise account security or data integrity.

Acceptance Criteria
First-Time Watch–Phone Pairing Issues Device-Bound Token
Given the user is signed in on the phone and StreakShare is installed on both phone and watch When the user initiates pairing from the phone and confirms on the watch Then mutual device attestation is attempted where supported and a unique device-bound key is generated And a scoped access token (read habits, create check-ins only) is derived, provisioned to the watch via an encrypted channel, and stored in the watch’s secure enclave/keychain And the phone stores only a pairing record and hashed token identifier (no token secret) And the token is non-exportable and cannot be read in plaintext And the server associates the token with the user, device fingerprint, and scope And the pairing flow completes in ≤ 15 seconds on a typical mobile connection And any attestation or secure-storage failure aborts pairing and surfaces a clear retryable error
Authorize Raise-to-Check with Scoped Token
Given a paired watch has a valid, unexpired, and non-revoked device token When the user performs Raise-to-Check and taps confirm on the watch Then the watch calls the check-in API using the device token and a fresh signed request And the server authorizes only the scopes read:habits and write:checkins; any other endpoint returns 403 And a check-in is created and persisted once, returning 201 within ≤ 2 seconds P95 And the watch delivers a single discreet success haptic on 2xx and no check-in is created on 4xx/5xx And no personal data beyond what is required for the check-in is transmitted
Token Revocation on Unpair or Sign-Out
Given a watch was previously paired to the user’s account When the user unpairs the watch from the phone OS or signs out of StreakShare on the phone Then the server revokes the device token within ≤ 5 seconds of the event And the watch deletes the token from secure storage within ≤ 10 seconds of next connectivity And subsequent API requests with the revoked token return 401 with error code TOKEN_REVOKED And an audit event device_token_revoked is recorded with actor, reason, and timestamps And the watch UI reflects a revoked state and prompts to re-pair on next action
Rate Limiting and Abuse Detection for Device Tokens
Given a device token is making API requests When more than 10 check-in create attempts occur within 60 seconds Then the server returns 429 Too Many Requests and logs a rate_limited event And when 3 attestation validation failures occur within 5 minutes for the same device Then the token is suspended for 15 minutes and requests return 403 SUSPENDED And normal usage (≤ 3 check-ins/day and ≤ 30 habit reads/hour) is unaffected and returns 2xx And rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After) are sent where applicable And abuse detections are written to audit logs with token_id (hashed) and correlation_id
Mutual Attestation During Pairing (Platform-Supported)
Given both phone and watch support platform attestation APIs When the user initiates pairing Then the phone presents a server-verifiable attestation including app integrity and a server-provided nonce And the watch presents an attestation relayed via the phone with a distinct nonce And the server validates signatures, nonce freshness (≤ 120 seconds), device class, and OS versions against policy And pairing proceeds only if both attestations are valid; otherwise pairing is blocked with ATT_INVALID And attestation inputs and results are recorded with a correlation_id for traceability
Audit Logging of Check-Ins with Device Metadata
Given a check-in is created from a paired watch When the server processes the request Then an immutable audit record is written with user_id, habit_id, device_id, token_id (hashed), device_type=watch, app_version, os_version, IP, timestamp (UTC), attestation_result, rate_limit_action, correlation_id And audit records are retained ≥ 365 days and are queryable by authorized staff in ≤ 2 seconds P95 And the user’s data export includes their check-in audit entries with device metadata
Replay Protection and Idempotent Check-In Creation
Given the watch sends a check-in request When the request includes a signed nonce and an idempotency key unique per habit per UTC day Then the server validates signature and nonce freshness (clock skew ≤ 60 seconds) and processes at most one check-in per idempotency key And duplicates or stale nonces return 409 Conflict without creating additional check-ins And all requests are transmitted over TLS 1.2+; invalid signatures return 401 without side effects

Widget Blur

A privacy-first lockscreen mode that hides room names and exact times until you unlock. You still get a clear one-tap “Check In” button and fuzzy time ranges, so you can act fast while keeping habits and identity out of sight in public.

Requirements

Lock Screen Blur Display
"As a privacy-conscious user, I want the widget to hide my room names and exact times on the lockscreen so that I can maintain privacy in public without losing the ability to act quickly."
Description

Implement a lockscreen widget state that hides room names, participant details, and exact start times until the device is unlocked. The widget displays a generic habit icon, a neutral label (e.g., “Upcoming Habit”), a fuzzy time indicator, and a prominent “Check In” button. It must present zero personally identifiable or sensitive habit information on the lockscreen while maintaining immediate actionability. Supports iOS and Android lockscreen widget frameworks, honors system light/dark modes, and gracefully degrades on devices without lockscreen widget support. Ensures no sensitive strings are embedded in the widget snapshot cache.

Acceptance Criteria
Lockscreen Hides Sensitive Details
Given StreakShare lockscreen widget is enabled and the device is locked When the widget renders Then it displays only a generic habit icon, the neutral label "Upcoming Habit", a fuzzy time indicator, and a "Check In" button And it does not display room names, habit titles, participant names/avatars, or exact start times And no sensitive details are present in widget text, content descriptions, or accessibility labels And the deep link behind "Check In" contains no readable PII (only opaque IDs/tokens)
Fuzzy Time Indicator on Lockscreen
Given an upcoming habit scheduled for a specific time When the widget renders on the lockscreen Then the time indicator is fuzzy (e.g., "in ~15m", "this evening", "soon") and never shows an exact time (e.g., "3:17 PM") And the fuzzy value is computed using the device's local timezone And the fuzzy label updates at least every 15 minutes while on-screen without revealing exact times And localization uses generic, privacy-preserving phrases with no habit-specific words
Prominent One-Tap Check In From Lockscreen
Given the device is locked and an upcoming habit exists When the user taps the "Check In" button on the lockscreen widget Then the OS prompts for unlock (biometric/PIN) and, upon successful unlock, navigates directly to the Check-In confirmation screen for the next habit And no sensitive information is shown on the lockscreen prior to unlock during this flow And the tap target for "Check In" meets platform guidelines (>=44pt iOS, >=48dp Android) And the time from unlock to Check-In screen load is <= 2 seconds on a median device Given multiple eligible habits at the same time When the user taps "Check In" Then after unlock, a selection screen is shown with details, while the lockscreen widget remains generic
System Theme Compliance and Accessibility
Given the system is set to light or dark mode When the widget renders on the lockscreen Then the widget adapts its background/foreground to the system theme and maintains text/icon contrast ratio >= 4.5:1 Given VoiceOver/TalkBack is enabled When focus moves to the widget elements Then the spoken labels are generic and contain no habit names, participant names, or exact times (e.g., "Upcoming habit, check in button, in about fifteen minutes") Given dynamic type/font scaling up to 120% When the widget renders Then labels do not clip and the "Check In" button remains fully visible and tappable
Graceful Degradation Without Lockscreen Widget Support
Given a device/OS that does not support lockscreen widgets When the user enables Widget Blur in settings Then the app does not attempt to add a lockscreen widget and offers a generic, non-PII notification as an optional fallback And the fallback shows only a neutral label and fuzzy time, with a secure deep link requiring unlock before showing details And the app does not crash or show errors; settings clearly indicate the limitation
No Sensitive Strings in Widget Snapshots and Resources
Given the widget has rendered on the lockscreen When system snapshot caches are inspected (e.g., iOS widget snapshots, Android RemoteViews caches) Then no room names, participant names, habit titles, or exact times are present; only generic labels and fuzzy times appear Given the app binaries and widget bundles are scanned When searching strings/resources Then no sensitive dynamic values are embedded in compiled resources or persistent widget timelines; placeholders/tokens are used instead Given the app switcher or preview surfaces show the widget When previews are captured Then no sensitive strings appear in previews before unlock
One‑Tap Check‑In (Locked State)
"As a busy remote worker, I want to check in with one tap from my lockscreen so that I can keep my streak without unlocking or navigating the app."
Description

Enable a single-tap check-in action directly from the lockscreen widget without revealing hidden details. The action resolves the relevant room/session server-side, validates eligibility (time window, membership), and records the check-in. Provide discreet success/failure feedback (icon change, subtle haptic) that does not disclose sensitive context. Supports offline queuing with automatic retry, respects OS authentication prompts when required, and never displays redacted content prior to unlock.

Acceptance Criteria
Locked Online Eligible Check-In
Given the device is locked, the StreakShare widget is in blur mode, network is reachable, and the user is a member of exactly one room with an active check-in window now When the user taps the widget "Check In" button Then a check-in request is sent to the server without opening the app And the server validates eligibility and records a single check-in for the resolved session And the widget shows a discreet success state (icon change) within 1 second of server acknowledgment and plays a light success haptic And the widget does not reveal room name, avatar, or exact times at any point And the recorded check-in is visible in-app after unlock with the correct server timestamp and associated room And the end-to-end time from tap to success state is 5 seconds or less under typical network conditions
Locked Ineligible Check-In Handling
Given the device is locked, the StreakShare widget is in blur mode, and network is reachable, but the user has no eligible session OR is not a member of any eligible room When the user taps the widget "Check In" button Then the server rejects the operation as ineligible and no check-in is recorded And the widget displays a discreet generic failure state within 1 second and plays a light error haptic And no reason, room name, or exact times are shown on the lockscreen And the widget reverts to its neutral state within 3 seconds without revealing sensitive context
Privacy: No Sensitive Details on Lockscreen
Given the device is locked and the StreakShare widget is visible When the widget is idle, during, or after a check-in attempt (success or failure) Then the widget displays only generic UI: app icon/name, a "Check In" control, and a fuzzy time label (e.g., "Now", "This hour"), not exact times or room identifiers And room names, avatars, exact start/end times, participant names, and streak counts are not present in visible text or accessibility labels/hints And post-tap feedback uses only generic iconography and haptics with no sensitive text And unlocking the device is required before any redacted content can be viewed
Offline Queue and Automatic Retry
Given the device is locked and has no network connectivity When the user taps the widget "Check In" button Then a single offline check-in operation is enqueued with a stable idempotency key and the original tap timestamp And the widget indicates a queued/pending state within 500 ms with a neutral haptic, without revealing sensitive details And upon network restoration, the client automatically retries until success or 24 hours elapse, making at least 3 attempts within the first 10 minutes And on success, the server records a single check-in using the original tap timestamp for createdAt, the queue is cleared, and the widget shows a discreet success state And if retries exhaust, no check-in is recorded, the widget returns to neutral without sensitive error details, and the pending failure is visible only after unlock
Server-Side Session Resolution Determinism
Given the device is locked, the user is a member of multiple rooms with overlapping eligible windows When the user taps the widget "Check In" button Then the server deterministically selects exactly one session using this precedence: (1) windows that include now; tie-breaker A: earliest window end time; tie-breaker B: highest widget priority order; tie-breaker C: lowest session ID And a single check-in is recorded for the selected session and no ambiguity is revealed on the lockscreen And the in-app activity log after unlock shows the selected room/session consistent with the precedence rules
OS Authentication Prompt Respect
Given the OS requires biometric/passcode authentication to allow the network request or access secure credentials while locked When the user taps the widget "Check In" button Then the OS-native authentication prompt appears without exposing StreakShare room names, exact times, or other sensitive details And upon successful authentication, the check-in proceeds and completes without opening the app; upon cancel/fail, no check-in is recorded And the widget returns to an appropriate generic state (success or neutral) within 2 seconds and never reveals redacted content prior to full unlock
Idempotency Under Rapid Taps
Given the device is locked and the user taps the widget "Check In" button multiple times within 10 seconds (online or offline) When the first tap is processed or enqueued Then exactly one check-in operation is created using a stable idempotency key valid for at least 10 seconds And no duplicate check-ins are recorded server-side; additional taps do not create additional queue items And the widget provides at most one brief haptic/visual feedback cycle and does not flicker or reveal sensitive context
Fuzzy Time Ranges
"As a user in public, I want to see approximate timing instead of exact timestamps so that I can act quickly without exposing my schedule."
Description

Replace exact times with privacy-preserving, human-friendly ranges (e.g., “now,” “in ~5–10 min,” “this morning”) on the lockscreen widget. The formatter localizes language, respects 12/24‑hour preferences implicitly, and adjusts granularity by proximity to the event. It handles edge cases like overdue windows (“ran ~5 min ago”) and unscheduled days. Provide a configuration for product to tune thresholds and buckets without app updates.

Acceptance Criteria
Near-Term Upcoming Window Fuzzy Label
Given the lockscreen widget is visible and a room’s next check-in window is upcoming And the fuzzy-time configuration defines a minutes bucket labeled “in ~5–10 min” for the range [5,10] When the scheduled start is 7 minutes from now Then the widget displays the localized label for that bucket And the label contains no absolute clock time (e.g., no colon-delimited time or AM/PM markers) And the label updates to the next configured bucket no later than 1 minute after crossing the bucket boundary
Recent Past (Overdue) Window Fuzzy Label
Given the fuzzy-time configuration includes past buckets such as [3,10] => “ran ~5–10 min ago” and [60,1440] => “earlier today” When the scheduled start occurred 5 minutes ago Then the widget displays the localized label “ran ~5–10 min ago” And the label contains no absolute clock time When the elapsed time reaches 65 minutes Then the label changes to the configured “earlier today” (or equivalent) by the next eligible refresh, no later than 1 minute after crossing the boundary
Unscheduled Day Fuzzy State
Given the user has no scheduled check-in window for the room today When viewing the lockscreen widget Then the time label shows the configured unscheduled string (e.g., “no session today”), localized to the device locale And no absolute or relative time is shown And if a localization is missing, a neutral fallback string from defaults is displayed
Localization, Dayparts, and 12/24 Preference
Given the device locale is set to fr-FR When the next window is 1 minute away Then the label uses correct French singular/plural grammar (e.g., “dans ~1 min” vs “dans ~5–10 min”) and localized dayparts (e.g., “ce matin/ cet après-midi”) when applicable per configuration And numerals and separators follow the locale conventions And when the user prefers 24-hour time, no AM/PM markers appear in any label And all strings are sourced from localizable resources, not hard-coded
Granularity Transitions by Proximity
Given the configuration defines bucketed ranges for minutes, hours, and dayparts When the event transitions from 3 hours away to 1 hour 59 minutes away (crossing a configured boundary) Then the label changes from a coarse label (e.g., a daypart or hour-level label) to the next finer-grained bucket by the next eligible refresh (≤ 1 minute) And the label does not oscillate while time remains within the same bucket And there are no more than one label change per minute due to proximity rounding
Remote Configuration, Update, and Fallback
Given the app can fetch fuzzy-time configuration (buckets, thresholds, labels) from a remote source with a configurable TTL When a new configuration is published server-side Then the widget applies it without requiring an app update by the next config refresh and subsequent widget refresh And if the fetch fails, the last-known-good configuration is used; if none exists, a baked-in default is used And configuration changes take effect at runtime without requiring a device unlock or app relaunch And invalid or incomplete configurations are rejected and do not break label rendering
Privacy Controls & Overrides
"As a creator managing multiple habits, I want granular privacy settings for the widget so that I can balance discretion with usefulness across different rooms."
Description

Add global and per-room controls for Widget Blur behavior: toggle blur mode, choose which elements to hide (room name, streak count, participant avatars), and allow trusted rooms to display limited context after unlock. Defaults to maximum privacy. Settings sync with the user’s account, respect device-level privacy settings, and support remote kill‑switch for compliance issues. Clear in‑app previews show how the widget will appear when locked vs. unlocked.

Acceptance Criteria
Global Blur Toggle & Default Maximum Privacy
- Given a first-time login or no prior preference after update, When the app initializes, Then global Blur Mode is ON by default and room name, streak count, and participant avatars are hidden on the lock screen widget. - Given global Blur Mode is ON, When the user views the lock screen widget, Then only the "Check In" button and fuzzy time range are visible; no room names, streak counts, participant avatars, or other PII are rendered. - Given the user turns global Blur Mode OFF in Settings, When the device is locked, Then previously hidden elements are shown on the widget within 2 seconds, unless restricted by device-level privacy or an active kill-switch. - Given the user turns global Blur Mode ON in Settings, When the device is locked, Then hidden elements are concealed within 2 seconds.
Per-Room Element Visibility Controls & Precedence
- Given a room's privacy settings, When the user sets Hide Room Name=ON, Hide Streak Count=OFF, Hide Avatars=ON, Then the lock screen widget for that room reflects these choices while global Blur Mode is ON. - Given per-room settings would reduce privacy compared to global, When the device is locked, Then global privacy prevails and the widget does not reveal more than allowed by global settings. - Given per-room settings increase privacy compared to global, When the device is locked, Then the widget honors the stricter per-room settings. - Given the user changes a per-room toggle, When they return to the lock screen, Then the widget updates within 2 seconds and the setting persists after app relaunch.
Trusted Rooms Limited Context After Unlock
- Given a room is marked Trusted with limited-context options (e.g., Show Room Name=ON, Show Streak Count=ON, Show Avatars=OFF), When the device is unlocked and the user is authenticated, Then the widget shows only the configured limited context for that room while keeping non-selected elements hidden. - Given a room is not Trusted, When the device is unlocked, Then the widget continues to apply global/per-room privacy and does not show limited context. - Given the user removes a room from Trusted, When the device is unlocked, Then limited context is no longer displayed for that room within 2 seconds.
Settings Sync Across Devices
- Given the user enables Hide Avatars for Room A on Device A, When Device B is online and the account is in the foreground, Then the same setting appears on Device B within 60 seconds or next app foreground, whichever comes first. - Given conflicting edits on different devices, When both changes are synced, Then the server resolves by last-write-wins using server timestamps and the final state is reflected on all devices within 60 seconds. - Given a fresh install, When the user signs in, Then their latest privacy and trusted-room settings are restored before the first widget render.
Respect Device-Level Lock Screen Privacy
- Given the device-level setting to hide sensitive content on the lock screen is ON, When the device is locked, Then StreakShare widgets hide room names, streak counts, and participant avatars regardless of app settings. - Given the device-level setting is OFF, When the device is locked, Then the widget displays content as allowed by the app’s global/per-room/trusted settings. - Given the device requires biometric to reveal lock screen content, When content has not been unlocked biometrically, Then the widget remains in maximum-privacy mode.
Remote Kill-Switch Enforcement
- Given the backend activates the privacy kill-switch for the user’s region/account, When the app next fetches config or receives a push, Then the app enforces maximum privacy within 5 minutes: global Blur Mode ON, all elements hidden, trusted-room limited context disabled. - Given the kill-switch is active, When the user attempts to change privacy settings, Then controls are disabled or overridden with an explanatory notice, and no restricted content appears on the lock screen. - Given the kill-switch is deactivated, When the app next fetches config, Then prior user settings are restored and controls become editable.
In-App Locked vs Unlocked Previews
- Given the user opens Privacy Controls, When they toggle between "Locked Preview" and "Unlocked Preview," Then the preview accurately reflects the current global/per-room/trusted settings for each state. - Given the user changes a setting (e.g., Hide Streak Count), When viewing the previews, Then both previews update within 1 second to reflect the new state. - Given the device-level privacy setting would override app settings, When viewing "Locked Preview," Then the preview indicates the override and shows maximum-privacy rendering.
Accessibility‑Safe Widget
"As a user who relies on assistive technologies, I want the lockscreen widget to be accessible and discreet so that I can check in confidently without exposing private information."
Description

Ensure the lockscreen widget meets accessibility standards without leaking redacted details. VoiceOver/TalkBack labels use generic, non-sensitive descriptions; focus order prioritizes the Check In action; tap targets meet minimum size; color contrast meets WCAG; and haptic/visual feedback is perceivable in bright outdoor conditions. Provide localized accessibility strings and test across common screen reader configurations.

Acceptance Criteria
Screen Reader Safe, Generic Labels
Given the widget contains redacted room names and exact times And VoiceOver or TalkBack is enabled When the user focuses each widget element Then accessible names, hints, and values are generic and contain no room names, exact times, or personal identifiers And the primary control is announced as "Check In, button" And the time context is announced as a fuzzy range (e.g., "morning window") without specific timestamps And no custom actions or accessibility labels expose sensitive text
Focus Order Prioritizes Check In
Given VoiceOver/TalkBack is enabled on the lock screen When the user swipes right from the widget's first focusable element Then the focus order is: Check In button, Fuzzy time label, Streak indicator (if present), Privacy info (if present) And hidden or decorative elements are not focusable And Check In is reachable within 2 swipes from initial widget focus
Minimum Tap Targets on Lockscreen Widget
Rule: The Check In control has a minimum hit area of 44x44pt (iOS) and 48x48dp (Android) Rule: Adjacent touch targets are separated by at least 8pt/dp Rule: Hit area includes invisible padding so the effective touch region meets the minimum even if the visual element is smaller Rule: Targets remain compliant on small lockscreen layouts and varying system font sizes
WCAG AA Contrast and Non‑Text Contrast
Rule: All text and essential icons meet WCAG 2.1 AA contrast ratios (≥4.5:1 for normal text, ≥3:1 for large text) Rule: Non-text UI components and focus/pressed/checked states meet ≥3:1 contrast against adjacent colors per WCAG 2.1 1.4.11 Rule: The checked-in visual state maintains ≥3:1 contrast difference from the pre-check-in state and is discernible in bright outdoor conditions Rule: Background blur/translucency does not reveal redacted details under light or dark themes
Perceivable Haptic and Visual Feedback Outdoors
Given the device supports haptics and the user taps Check In Then a confirmation haptic is emitted within 100 ms of activation using the platform’s standard confirmation pattern And a visual confirmation (e.g., checkmark/state change) appears within 200 ms and persists for at least 1.5 s And if haptics are disabled or unavailable, the visual confirmation still occurs without errors And a single generic announcement "Checked in" is spoken once without repetition
Localized Accessibility Strings Without Sensitive Data
Rule: Accessibility labels, hints, and announcements are localized for en, es, fr, de, pt-BR, ja, and ar Rule: Localized strings are generic and never include room names, exact times, or personal identifiers Rule: Pseudo-localization test passes without truncation, clipping, or overflow within widget bounds Rule: When a locale is unsupported, English strings are used as fallback with no placeholders or untranslated tokens
Cross‑Configuration Screen Reader Operability
Given iOS 17+ with VoiceOver and Android 13+ with TalkBack When testing with hints on/off, focus follows touch on/off, and speech rates between 50% and 80% Then the Check In control remains discoverable and actionable And total swipes to reach Check In from widget entry is ≤3 And no duplicate or conflicting announcements occur during focus or activation
Secure Ephemeral Actions
"As a security‑minded user, I want lockscreen actions to be cryptographically protected so that my account and habits remain secure even if my phone is visible or temporarily handled by someone else."
Description

Protect lockscreen actions with short‑lived, scoped tokens generated on-device and validated server-side with replay protection and rate limiting. No long‑term secrets are stored in the widget. All action payloads exclude sensitive fields and are encrypted in transit. Implement server auditing with redacted logs and alerts on anomalous attempts. If token validation fails, require unlock or re-authentication without revealing context.

Acceptance Criteria
Ephemeral Token TTL and Single-Use
Given the device is on the lock screen with the StreakShare widget active When the user taps the Check In button Then the widget generates an on-device scoped token with an issued-at timestamp and a TTL of 60 seconds or less And once a token is successfully validated server-side, any subsequent use of the same token is rejected And if the token is older than its TTL, the server rejects it as expired And no token material is persisted to disk, keychain, or shared preferences; killing/restarting the widget process clears any in-memory token
Server Replay Protection Validation
Given a token has been used once to perform a successful check-in When the same token is presented again to the validation endpoint Then the server returns 409 Conflict and records no additional check-in And a replay-prevention record (e.g., nonce/jti) with appropriate TTL is stored to prevent cross-node reuse And the audit log records a replay_attempt event without storing the raw token value
Scoped Tokens for Check-In Only
Given a token scoped to the check_in action and a specific user/room context embedded in the token claims When that token is used against any endpoint other than check-in Then the server returns 403 Forbidden and logs a scope_mismatch event without revealing identifiers And when the token is presented for any user or room other than its embedded scope Then the server returns 403 Forbidden without disclosing target identifiers And the widget action payload includes only {token, device_nonce, fuzzy_time_bucket}; it does not include user_id, email, username, room_name, room_id, or exact timestamps
Transport Encryption Enforced
Given a network inspection environment When the widget sends a token validation request Then the request is sent only over HTTPS with TLS 1.2 or higher and no plaintext packets are observable And attempts to downgrade to HTTP or TLS <1.2 result in connection failure with no request payload transmitted And connections using invalid or untrusted certificates are rejected and no sensitive data is sent
Rate Limiting on Token Validation
Given more than 5 invalid token validation attempts originate from the same device within 60 seconds When the 6th attempt occurs within that window Then the server responds with 429 Too Many Requests, includes a Retry-After header, and makes no state changes And given more than 20 total validation attempts (valid + invalid) from the same device within 60 seconds When subsequent attempts occur Then the server throttles to at most 1 validation per second for the next 120 seconds and logs a rate_limit event
Auditing with Redacted Logs and Anomaly Alerts
Given any token validation attempt When the server writes an audit entry Then the entry contains timestamp, endpoint, result code, reason category, and a device identifier hash; it must not contain the raw token, user identifiers, room names, or exact times And given 10 or more invalid/replay attempts from a single device within 60 seconds When the threshold is crossed Then an alert is sent to the security notification channel within 60 seconds with redacted context (no PII or token values)
Failure Handling Requires Unlock Without Context Leakage
Given token validation fails for any reason (expired, invalid, replay, or rate-limited) When the widget renders on the lock screen Then the UI displays a generic prompt such as "Unlock to continue" without revealing room names, usernames, or exact times And the server response body contains only a generic error code and no contextual identifiers And after the user unlocks or re-authenticates, a new token is generated and the action can be retried without exposing prior context on the lock screen
Low‑Latency, Battery‑Safe Widget
"As a daily user, I want the widget to be responsive and battery‑friendly so that I can rely on quick check‑ins without draining my device."
Description

Optimize the widget for quick render and minimal power usage: precompute next relevant session and fuzzy time text, cache non-sensitive assets, respect OS background refresh budgets, and avoid frequent wake-ups. Target sub‑300 ms render time for the widget state change post check-in. Provide telemetry to monitor render time, crash rates, and battery impact without collecting sensitive content.

Acceptance Criteria
Sub-300ms Widget Re-render After Check-In
Given the user taps "Check In" on the widget When the check-in event is acknowledged by the app/service Then the widget UI reflects the new state within 300 ms at the 95th percentile across supported devices Given a widget state change has occurred When the widget updates Then the button state switches to "Checked In" (or disabled) and no loading indicator persists longer than 200 ms Given telemetry is enabled When a widget state change occurs Then render_duration_ms is recorded to verify <= 300 ms p95 and <= 150 ms p50 over a rolling 7-day window
OS Background Refresh Budget Compliance
Given the app is in background on iOS When scheduling widget updates Then reloadTimelines is invoked no more than 4 times per hour and calls are rate-limited to comply with OS throttling Given the app is in background on Android When scheduling periodic updates Then WorkManager intervals are >= 15 minutes and expedited jobs are used only on direct user action Given Low Power Mode (iOS) or Battery Saver (Android) is active When constraints are signaled by the OS Then periodic background updates are deferred; user-initiated interactions still execute Given daily operations over 7 days When telemetry aggregates background wake-ups Then background_wakeups_per_day <= 2 at median and <= 4 at p95
Precomputed Next Session and Fuzzy Time Text
Given the app enters foreground or a check-in completes When preparing the widget model Then the next relevant session timestamp/ID and its fuzzy time label are precomputed and cached Given local time crosses a fuzzy boundary When the widget refreshes Then the fuzzy label updates using precomputed rules without network calls Given precomputed data is older than 60 seconds When rendering the widget Then recomputation occurs off the main thread and render time remains within performance targets Given privacy requirements When persisting precomputed data Then no room names or exact timestamps are stored; only hashed IDs and rounded time ranges are kept
Non-Sensitive Asset Caching
Given the widget renders When loading icons and static UI assets Then cache hit rate is >= 90% over a 7-day rolling window per device Given cache growth over time When measuring storage Then total widget cache footprint <= 5 MB and uses LRU eviction Given cache content inspection When scanning cached keys and payloads Then no free-text room names or exact timestamps are present Given a cache miss occurs during render When fetching assets Then at most one network request is made per render and results are cached
Privacy-Safe Telemetry for Performance and Battery
Given telemetry is enabled When a widget render or state change occurs Then events include render_duration_ms, device_model, OS_version, app_version, process_uptime_ms, and battery_delta_mAh (or OS-estimated drain) and exclude room names, user names, and exact timestamps Given user disables telemetry When the widget renders or updates Then no telemetry events are transmitted Given crash and ANR monitoring over a 7-day window When aggregating metrics Then widget crash rate <= 0.1% per 10,000 renders and ANR rate <= 0.2% Given daily aggregation When analyzing battery impact Then median battery share attributable to the widget <= 1.5% and p95 <= 3% Given data governance When retaining telemetry Then retention is <= 30 days
Battery and Network Budgets Under Normal Use
Given normal daily use (3–5 renders, 1 check-in) When measured over 7 consecutive days Then average background wake-ups per day <= 2 and p95 <= 4 Given background data transfers When aggregating usage per day Then widget-related background data <= 1 MB/day at p95 and foreground interactions <= 3 MB/day at p95 Given CPU profiling during render When measuring across supported devices Then CPU time per render <= 50 ms at p50 and <= 120 ms at p95 Given memory profiling during render When measuring peak additional RSS Then widget-induced peak additional memory <= 40 MB on iOS and <= 60 MB on Android at p95

Room Carousel

Flip through your top rooms directly on the lockscreen widget or watch complication to see time left and check in or react with one tap. Manage multiple habits without app-hopping, reducing misses across a busy day.

Requirements

Lockscreen Room Carousel Widget
"As a remote worker, I want to swipe through my active rooms on my lockscreen and check in with one tap so that I don’t miss streaks during busy days."
Description

Interactive lockscreen widget that lets users swipe through their top StreakShare rooms, view time remaining in the current check-in window, current streak, and room badge/presence, and perform one-tap Check In or one-tap React. Uses interactive widget capabilities where supported (e.g., iOS 17+), with deep links for extended input. Refreshes on schedule boundaries and server push invalidations with strict background refresh budgets to preserve battery. Outcome: fewer missed check-ins, faster micro-commitments, and improved streak retention without opening the app.

Acceptance Criteria
Swipe Through Top Rooms on Lockscreen
Given the user has the StreakShare lockscreen widget installed on iOS 17+ and at least 2 top rooms configured When the user swipes left or right on the widget Then the widget displays the previous/next room within 250 ms and updates the room title, badge/presence, streak count, and primary actions for that room And the carousel wraps after the last room to the first and vice versa And if the user has only 1 top room, swipe is disabled and no pagination affordance is shown And no more than 5 rooms are shown in the carousel, ordered by the Top Rooms list provided by the app
Display Check-In Window Time Remaining
Given the current room has a defined check-in window When the current time is within the window Then the widget shows a countdown in hh:mm (mm:ss if < 60 minutes remaining) and updates at least once per minute (once per second if < 60 minutes) And at the exact end of the window the UI updates within 2 seconds to reflect Closed and disables Check In And when outside the window, it shows Opens in hh:mm until the next window start And after a system timezone or DST change, the countdown recalculates within 5 seconds
One-Tap Check In (Interactive and Fallback)
Given iOS 17+ interactive widgets and the room's check-in window is open and the user has not checked in When the user taps Check In on the widget Then the app sends a check-in request and shows success state (checkmark, disabled button, updated streak) within 1 second of network success And a deep link to the room opens only if additional input is required Given iOS 16 or non-interactive environments When the user taps Check In on the widget Then the StreakShare app opens via deep link directly to the room's check-in screen within 2 seconds
One-Tap React with Default and Deep Link
Given iOS 17+ interactive widgets and the room's check-in window is open When the user taps React on the widget Then the default reaction is sent to the current room and the reaction indicator updates on the widget within 1 second And a long-press on React opens the reaction palette via deep link for extended input Given non-interactive widgets When the user taps React on the widget Then the app opens via deep link to the reaction palette for the current room within 2 seconds And one-tap reaction from the widget is limited to one reaction per user per window
Content Refresh on Schedule Boundaries and Push Invalidations
Given a schedule boundary (open/close) occurs for any room shown in the carousel When the boundary time is reached Then the widget requests a timeline reload and reflects the new state (countdown, action availability, streak) within 60 seconds Given the server sends a push invalidation for a specific room When the notification is received Then the widget refreshes that room's data within 15 seconds And refreshes are rate-limited so no more than 1 refresh per room per minute is performed
Background Refresh Budget and Battery Constraints
Given normal operation over a 24-hour period with the widget installed and 3–5 top rooms configured When no push invalidations or schedule boundaries occur Then the widget does not poll more frequently than once every 30 minutes And total background data usage attributable to the widget is ≤ 500 KB/day And measured incremental battery impact attributable to the widget is ≤ 2% of device battery over 24 hours And network requests during background refresh are coalesced to a single batch per refresh event
Offline, Error, and Edge-Case Handling
Given the device is offline when the user taps Check In or React When the action is initiated Then the action is queued locally, a Queued indicator appears on the widget, and the action is sent within 30 seconds of connectivity restoration And after 3 failed retries the widget shows Failed with a deep link to retry in-app Given the user has zero top rooms configured When the widget is displayed Then it shows an Add Rooms call-to-action that deep links to room selection in-app Given the user loses permission to a room When the widget focuses that room Then actions are disabled and a No access state is shown until the room is removed from the carousel
Watch Carousel Complication
"As a watch-first user, I want to check in and react from my wrist with haptic confirmation so that I can maintain habits without pulling out my phone."
Description

Apple Watch and Wear OS complication plus lightweight glanceable app that surfaces the next actionable room and enables crown/gesture scrolling through rooms. Shows countdown ring to window close, streak count, and room badge. Supports single-tap Check In with haptic confirmation and a secondary tap for default reaction, with offline queuing and auto-sync on reconnect. Respects Focus/Do Not Disturb, workout modes, and complication size constraints.

Acceptance Criteria
Complication Shows Next Actionable Room
Given the user has multiple rooms with active check-in windows When more than one room is actionable Then the complication displays the room whose window closes soonest and is not yet checked in Given the current time crosses into a new room’s check-in window When the window opens Then the complication updates within 60 seconds to show that room Given a room is displayed on the complication Then it shows a countdown ring to window close (±1 minute accuracy), the current streak count as an integer, and the room badge icon Given the user has already checked in for a room today When selecting the next actionable room Then that room is excluded from selection
Glanceable App Crown/Gesture Carousel
Given the glanceable app is opened from the complication When the user rotates the Digital Crown (watchOS) or swipes vertically (Wear OS) Then the view cycles through rooms in the user’s Top Rooms list with wrap-around order Given the glanceable app loads Then the next actionable room is positioned first in the carousel Given the user scrolls between room cards Then scroll latency is under 100 ms and frame drops do not exceed 5% on target devices Given a room card is in focus Then it displays the countdown ring, streak count, and room badge
Single-Tap Check-In with Haptic Confirmation
Given a displayed room is within its check-in window and the user has not checked in When the user taps the primary action on the complication or glanceable app Then a check-in is recorded within 250 ms with a light haptic confirmation Given a successful check-in Then the UI updates to a checked-in state within 500 ms and prevents duplicate submissions for 2 seconds Given a room is checked in Then it is removed from being selected as the next actionable room on the complication within 60 seconds
Secondary Tap Triggers Default Reaction
Given a successful check-in occurred within the last 10 seconds When the user taps the primary action again Then the default reaction is attached to the latest check-in and a distinct haptic confirmation is emitted Given a default reaction has been sent via secondary tap Then only one default reaction is applied per check-in via this shortcut Given no default reaction is configured When a secondary tap occurs within 10 seconds of check-in Then no reaction is sent and a non-blocking notice indicates no default reaction is set
Offline Queue and Auto-Sync
Given the watch is offline When the user performs a check-in or default reaction Then the action is queued locally with room ID, action type, and watch timestamp Given multiple offline actions are performed Then up to 100 pending actions are retained in order; additional actions are rejected with a non-blocking warning Given connectivity is restored When the app detects network availability Then queued actions are transmitted in original order within 30 seconds and marked synced only after server acknowledgment Given the server reports a window closed When validating a queued check-in Then accept if the watch timestamp falls within the original window; otherwise mark failed and inform the user non-blockingly
Respect Focus/Do Not Disturb and Workout Modes
Given Focus or Do Not Disturb is active Then the complication and app suppress attention-grabbing haptics and sounds; visual updates remain available Given a workout is active Then check-in and reaction taps are allowed but produce minimal haptic feedback only and no modal interruptions Given Silent mode is on Then no sounds are played for check-ins or reactions
Complication Layout Fits Size Constraints
Given supported complication families/sizes (watchOS: Modular Small, Graphic Circular, Graphic Bezel; Wear OS: Small/Medium) Then the layout displays a countdown ring/progress arc, numeric streak count, and room badge without truncation or overlap Given light and dark modes Then text/iconography meets a minimum 4.5:1 contrast ratio against the background Given interactive elements in the glanceable app Then the Check In tap target is at least 44x44 pt (watchOS) or 48x48 dp (Wear OS) Given the complication and glanceable app render Then average frame time stays under 16 ms on supported devices
Real-time Sync & Offline Action Queue
"As a user on spotty connections, I want my actions from the widget or watch to sync reliably so that my streaks update correctly across devices."
Description

Bidirectional state sync between mobile app, lockscreen widget, and wearables so check-ins and reactions update instantly across surfaces. Implements push-based cache invalidation, background refresh windows, and an offline store-and-forward queue using idempotent action tokens and conflict resolution for late or duplicate actions. Includes user-visible status, retry with backoff, and rate limiting to minimize API and battery impact while ensuring streak accuracy.

Acceptance Criteria
Instant cross-surface check-in propagation
Given the phone app, lockscreen widget, and watch are online and authenticated for the same room When the user taps Check In on the lockscreen widget for that room Then the completed check-in and updated streak count are visible on the phone app and watch within 2 seconds without manual refresh And the server-assigned timestamp is identical across surfaces Given the phone app, lockscreen widget, and watch are online and authenticated for the same room When the user sends a reaction from the watch complication Then the reaction appears on the phone app and lockscreen widget within 2 seconds and reaction counts match across all surfaces
Offline queue with store-and-forward
Given the device is offline When the user taps Check In or sends a reaction in a room from any surface (app, widget, watch) Then the action is stored locally with a generated idempotency token and marked Pending in the UI within 100 ms And the pending action persists across app/widget/watch restarts and device reboots Given connectivity resumes When there are pending actions Then the client transmits them in original creation order within 5 seconds, using exponential backoff on failures (2s, 4s, 8s, 16s, max 5 attempts) And successful actions transition to Synced state and update streak/reaction counts accordingly
Idempotent action tokens prevent duplicates
Given a previously queued or sent action with token T exists for the same habit window and room When the same action is retried due to timeout or the user double-taps Then only one server-side record exists and the client displays a single check-in/reaction outcome And the server responds with idempotent success for duplicates and the client does not create additional local events Given multiple surfaces submit the same action concurrently with the same token When responses arrive in any order Then the client reconciles to one final action with status Synced and no duplicate increments to streak or reaction counts
Conflict resolution preserves streak accuracy
Given two check-ins are submitted to the same habit window from different devices with up to 90 seconds of device clock skew When the server reconciles using server time Then only one check-in is stored for that window and the streak count increments by 1, not 2 And all clients converge to the resolved state within 3 seconds of notification Given a check-in is submitted after the habit window closes while a valid in-window check-in already exists When reconciliation completes Then the streak count remains unchanged and the late action is marked Ignored on all clients
Push-based cache invalidation and background refresh
Given an action (check-in or reaction) is accepted on any surface for a room When the server emits a push invalidation for that room Then other registered clients receive the push within 2 seconds and fetch only the affected room delta (<20 KB) and update UI without opening the app Given push delivery fails for a client When the client enters a background refresh window Then it performs a fallback delta sync for impacted rooms at most once every 15 minutes until push resumes
User-visible sync status and retry controls
Given an action is Pending or Retrying When the user views the room from the app, lockscreen widget, or watch Then the action shows a status indicator (Pending, Retrying with countdown, Synced, or Failed with reason) and a last-attempt timestamp Given an action is Failed due to a transient error When the user taps Retry Then an immediate retry is attempted respecting rate limits, and on success the status changes to Synced and the UI reflects the updated streak/reaction within 2 seconds
Rate limiting and battery impact constraints
Given the user performs no actions and is a member of up to 8 rooms When the app remains idle for 24 hours Then background sync traffic does not exceed 1 MB and background refreshes do not exceed 12 per hour on each device Given multiple local actions occur within a 300 ms interval When syncing Then they are batched into a single network request without delaying the first send beyond 300 ms Given the app is idle When receiving only push invalidations Then the network radio is not woken more than once per minute on average over any rolling 1-hour window
Room Prioritization & Scheduling Logic
"As a multi-habit user, I want the carousel to prioritize rooms that are due now so that I focus on the most urgent check-ins."
Description

Deterministic, server-assisted ranking that selects up to N top rooms for the carousel based on active/next windows, user pinning, historical adherence, and recency. Time-zone aware with DST handling, Focus/DND sensitivity, and per-room schedules. Provides fallbacks when no rooms are active (e.g., show next upcoming with ETA) and exposes remote-config thresholds so ordering and N can be tuned without app updates.

Acceptance Criteria
Deterministic Ranking Consistency Across Devices
Given a user is signed in on two devices with identical room memberships and schedules and the server ranking API returns the same config version, When both devices request the carousel within the same 60-second window, Then the selected top N room IDs and their order are identical on both devices, And across 10 consecutive refreshes over 10 minutes with no input changes, the order remains unchanged, And ties are resolved using a server-provided stable tiebreaker (e.g., roomId ascending) so results are deterministic.
Pinning and Active Window Precedence
Given multiple rooms with overlapping schedules and at least one pinned room is within an active window, When the carousel is ranked, Then all pinned active rooms appear before any unpinned rooms, And pinned active rooms are ordered by time-to-window-end ascending, And remaining active rooms follow ordered by composite score, And if fewer than N active rooms exist, the remaining slots are filled by next-window rooms ordered by time-to-start ascending, And the final list contains at most N rooms where N is read from remote-config.
Adherence/Recency Weighted Ordering
Given no pinned rooms are active, When ranking active rooms, Then each room’s rank is determined by a composite score defined as score = a*adherence + b*recency + c*(1 - normalized_time_to_end), And the weights a, b, c are supplied via remote-config and echoed in the server response metadata, And changing the weights in remote-config updates ordering within 5 minutes without an app update, And ties are broken deterministically using the server tiebreaker.
Time Zone and DST Accurate Scheduling
Given a user’s per-room schedules are defined in local time, When the device time zone changes or a DST transition occurs, Then the server normalizes and computes active/next windows so that window boundaries match the intended local times, And no computed boundary shifts by more than ±60 seconds due to conversion, And during a DST forward jump, skipped local times are not considered active, And during a DST fall-back, duplicated hours are treated as a single continuous window without double-counting, And a user traveling between time zones sees windows recomputed against the new local zone on the next sync.
Focus/DND-Respectful Surfacing
Given the device is in an active Focus/Do Not Disturb mode that suppresses StreakShare, When the carousel is rendered, Then rooms marked as quiet-respect are excluded from display while Focus is active, And their slots are backfilled by the next eligible rooms so up to N are shown, And a discreet Quiet badge replaces reactions for any partially suppressed items in allowed contexts, And within 60 seconds of Focus ending, eligible rooms reappear if still within active/next windows.
No-Active-Room Fallback with ETA
Given no rooms are in an active window, When generating the carousel, Then the next upcoming room is shown with an ETA in minutes and its local start time, And if multiple rooms start within the next 2 hours, show up to N upcoming rooms ordered by time-to-start ascending, And if the user has zero rooms configured, show the empty-state placeholder instead of an ETA list, And switching from no-active to active updates the carousel on the next refresh within 60 seconds.
Remote-Config Tunable N and Ordering
Given remote-config updates N (max rooms) and threshold parameters used by the server ranking, When the client fetches new config and the server applies it, Then the carousel reflects the new N within 5 minutes without requiring an app update, And ordering changes take effect on the first response computed with the new thresholds, And each ranking response includes the config version used for auditability.
Quick Reaction Picker
"As a participant, I want a quick way to send my usual reaction from the widget so that I can encourage others without opening the app."
Description

Lightweight reaction UX optimized for lockscreen and watch: single-tap sends the user’s per-room default reaction; long-press or secondary gesture opens a compact picker with recent and room-recommended emojis. Includes haptic feedback, accessibility labels, and a brief undo option. Persists per-room defaults and recents and deep-links to the full in-app reaction sheet when a larger set is needed.

Acceptance Criteria
One-tap default reaction from lockscreen/watch
Given a room is visible in the Room Carousel on the lockscreen widget or watch When the user single-taps the reaction target and a per-room default reaction exists Then the default reaction is sent to that room and the UI shows a success state within 500 ms and a confirmation haptic is played Given the reaction was sent When the server acknowledges receipt Then the reaction count and streak indicators update within 2 seconds Given no per-room default reaction exists When the user single-taps the reaction target Then the compact reaction picker opens instead of sending Given the user single-taps rapidly multiple times within 1 second When debouncing is applied Then only one reaction send occurs
Long-press compact reaction picker
Given a room is visible in the Room Carousel on the lockscreen widget or watch When the user long-presses (≥300 ms) the reaction target Then a compact reaction picker appears anchored to the reaction target within 300 ms Given the compact picker is open When displayed Then it shows recent emojis for this room and room-recommended emojis, with the per-room default pre-highlighted if present, totaling 6–8 options Given the compact picker is open When the user taps an emoji or press-and-drags and releases over an emoji Then that emoji is sent as the reaction and the picker dismisses Given the compact picker is open When the user taps outside the picker or presses the appropriate system dismiss gesture Then the picker dismisses with no reaction sent Given haptics are enabled When the compact picker opens or an option is highlighted Then a light haptic tick is produced
Deep link to full reaction sheet
Given the compact picker is open When the user taps the More option Then StreakShare opens to the full in-app reaction sheet for the same room within 1 second and focus is placed on the reaction grid Given the full reaction sheet is shown When the user selects a reaction and confirms Then the selected reaction is sent and, if the user chooses Set as default, the per-room default is updated Given the user returns to the widget or watch surface When StreakShare is backgrounded again Then the Room Carousel resumes at the same room context
Undo recently sent reaction
Given a reaction is sent from the widget or watch When the send completes (immediately or after queuing) Then an Undo affordance appears for 3 seconds with a subtle haptic Given the Undo affordance is visible When the user taps Undo within 3 seconds Then the reaction is removed or canceled, counts and streak indicators revert, and a confirmation haptic is played Given the undo operation encounters a network error When retries are attempted Then the UI indicates restoring and retries for up to 30 seconds before showing a non-blocking error state
Persist per-room defaults and recents
Given the user sets or changes a per-room default via the compact picker or full sheet When the selection is confirmed Then the default is saved for that room and used for future single-taps from both the widget and watch Given reactions are sent in a room When the user opens the compact picker for that room Then the last 5 reactions for that room are shown under Recents, ordered by most recent Given the app, widget, or watch surface is relaunched or the device is rebooted When the user returns Then per-room defaults and recents remain intact Given a paired watch is connected When the per-room default is changed on the phone Then the new default is reflected on the watch within 60 seconds
Accessibility support for quick reactions
Given a screen reader is enabled (VoiceOver on iOS; VoiceOver on watchOS) When focus moves to the reaction target or items in the compact picker Then descriptive accessibility labels and hints are announced, including room name and action (e.g., “Send thumbs-up to Daily Writing room”) Given large text or reduced motion is enabled When the quick reaction UI is displayed Then text scales appropriately and motion is reduced without loss of functionality Given interactive elements are presented When measured Then each hit target is at least 44x44 points and icons meet a minimum 4.5:1 contrast ratio against their backgrounds
Reliable send and failure handling
Given the device is offline When the user attempts to send a reaction Then the reaction is queued locally, the UI shows a queued state, and the send is retried automatically when connectivity returns Given transient network failures occur When sending a reaction Then up to 3 retries with exponential backoff are attempted before a non-blocking error state is shown with a Retry action Given duplicate taps or network retries occur When processing reaction sends Then reactions are idempotent per user-room and timestamp window, preventing duplicate records
Privacy & Lock-Safe Display Controls
"As a privacy-conscious user, I want to hide room details on my lockscreen so that sensitive habit info isn’t exposed."
Description

Privacy controls for surfaces visible without unlock. Provides a global and per-room toggle to hide details on lockscreen/complication, replacing names, avatars, and messages with generic placeholders and suppressing reaction content until authenticated. Reduces data-at-rest in widget/extension storage, signs requests with short-lived tokens, and supports biometric-gated reveal to prevent accidental exposure of sensitive habit information.

Acceptance Criteria
Global Privacy Toggle Masks Lock Surfaces
Given the global Lock-Safe Privacy toggle is enabled And the device is locked And the user views the Room Carousel on the lockscreen widget or watch complication When the surface renders or refreshes Then room names, avatars, messages, and member names are replaced with generic placeholders And reaction content and counts are not displayed And check-in/react affordances display neutral icons/labels (no room-specific text) And a successful one-tap check-in returns a generic confirmation (icon/haptic) without revealing details And disabling the global toggle reveals full details on the next refresh And the toggle state persists across app relaunches and device reboots And changes propagate to widget and complication within 5 seconds of update
Per-Room Privacy Override Masks Specific Room
Given the global Lock-Safe Privacy toggle is OFF And a room’s Per-Room Privacy toggle is ON When the room appears in the Room Carousel on lockscreen or watch Then that room’s name, avatar, messages, and reactions are masked with placeholders And other rooms with Per-Room Privacy OFF are displayed normally when the device is unlocked And when the device is locked, masking still applies per the global/per-room settings And if the global toggle is ON, the room remains masked regardless of the per-room toggle state And changing a room’s toggle updates the widget/complication within 5 seconds And per-room toggle state persists across app relaunches and device reboots
Biometric/OS-Gated Reveal of Masked Content
Given content is masked on the lockscreen widget or watch complication When the user invokes Reveal (e.g., tap/long-press per platform) Then an OS authentication prompt (biometrics or passcode) is shown And upon successful authentication within 10 seconds, only the currently focused room’s details are revealed And the reveal session lasts a maximum of 30 seconds or until the device relocks or the user dismisses, whichever occurs first And after the session ends, the room re-masks automatically And after 3 failed authentication attempts within 60 seconds, Reveal is disabled for 60 seconds and content remains masked And if OS authentication is unavailable, the UI shows “Unlock to view” and remains masked
Widget/Extension Data-at-Rest Minimization and Purge
Given Lock-Safe Privacy is enabled When the widget/complication caches data Then it stores no PII (room names, avatars, messages, member names, reaction types) at rest And it stores only minimal non-sensitive metadata required for rendering (e.g., room ID, order, time windows, last-check-in timestamp) And data at rest is protected by OS-level encryption appropriate to the platform And toggling Lock-Safe Privacy ON purges any previously cached sensitive fields within 1 second And receiving a device lock event clears any in-memory sensitive caches within 1 second And logging out purges caches and tokens immediately And verification by inspecting the widget/extension storage shows no PII keys/values present
Short-Lived Token Signing for Widget/Complication Requests
Given the widget/complication needs to fetch or post data When it issues a network request Then the request includes a short-lived token (TTL ≤ 10 minutes) bound to the user/session And tokens are stored in memory only (not persisted at rest) and cleared on device lock, logout, or expiry And the server rejects expired/invalid tokens with 401, and the UI remains masked with a non-revealing error state And token rotation occurs on or before expiry; no more than one active token per surface at a time And clock skew of ±2 minutes is tolerated without revealing content And all requests use TLS; on failure the UI degrades gracefully without exposing content
Suppress Reaction Content Until Authenticated
Given the device is locked or a room is masked by privacy settings When the Room Carousel is visible on the lockscreen widget or watch complication Then reaction types, senders, and textual content are not displayed and are replaced by a neutral placeholder or hidden state And tapping React prompts OS authentication; if not authenticated, no reaction is sent and no content is revealed And upon successful authentication, reactions become visible only within the active reveal session window and are re-masked afterward And live updates received during a masked state do not render reaction details until authenticated
Telemetry, Health Metrics & Remote Config
"As a product owner, I want to measure and tune carousel usage via experiments so that we can improve streak retention."
Description

Instrumentation to measure carousel effectiveness and reliability: impressions, swipes, tap-through, check-ins/reactions from widget/complication, latency, errors, and battery impact. Outputs retention-oriented KPIs tied to streak decay reduction. Enables feature flags and A/B testing for layout variants, default reaction behavior, and prioritization rules via remote configuration, with consent, privacy compliance, and user opt-out controls.

Acceptance Criteria
Widget/Complication Impression Telemetry
Given the Room Carousel is visible on the lockscreen widget or watch complication for ≥1 second, when the visibility threshold is met, then an impression event is queued with event_id, user_pseudo_id, room_id, surface, experiment_id, app_version, and timestamp. Given the same widget visibility session, when multiple exposures occur without being fully hidden for ≥1 second, then only one impression is logged per session_id and surface. Given the device is offline, when an impression is queued, then it is persisted locally and retried until delivered or TTL of 72 hours expires, with idempotent delivery enforced by event_id. Given an impression is delivered, when the server receives a duplicate event_id, then the duplicate is discarded without side effects.
Swipe, Tap-Through, Check-In, and Reaction Attribution
Given a user swipes the carousel on the widget/complication, when the swipe gesture completes, then a swipe event is logged with direction, from_room_id, to_room_id, surface, and experiment_id. Given a user taps to open the app from the widget/complication, when the app foregrounds, then a tap_through event is logged and linked to the next app session via session_correlation_id. Given a one-tap check-in or reaction is performed from the widget/complication, when the action completes, then a check_in or reaction event is logged with room_id, action_type, reaction_type (if any), success flag, error_code (if any), and latency_ms. Given multiple rooms are available, when any action occurs, then the event is attributed to the correct room_id and experiment variant active at the time.
Latency and Error Health Metrics
Given telemetry events are generated, when they are sent to the server, then the client includes enqueue_timestamp and send_timestamp, and the server appends receive_timestamp for end-to-end latency computation. Given ≥1,000 interactions in a day, when metrics are aggregated, then dashboards expose p50/p90/p95 end-to-end latency for check-ins, reactions, impressions, and swipes with 1-minute granularity. Given an error occurs during widget/complication interaction, when the error is captured, then an error event logs error_code, operation, surface, retryable flag, and redacted stack info without PII. Given transient failures, when retry policy executes, then ≥99% of events are delivered within 5 minutes of first enqueue in a healthy network test environment.
Battery Impact Monitoring
Given the Room Carousel is enabled, when the widget/complication is used, then the client records battery_level_before/after per 24h window and per 100 interactions, OS-reported background refresh counts, and CPU time, and uploads as aggregated health metrics. Given remote-config battery sampling is enabled, when sampling conditions are met, then high-frequency energy metrics are sampled on ≤5% of user-days and only while consent=granted. Given a health report is generated, when it is uploaded, then dashboards show estimated battery drain per 100 interactions and per active user-day, segmented by device model and OS version.
Retention and Streak Decay KPIs
Given users enable the Room Carousel, when daily cohorts are formed, then the analytics pipeline computes D1/D7/D30 retention, average daily check-ins per enabled user, and streak-break rate per 7-day window. Given a control cohort is available via feature flags, when weekly reports run, then uplift in check-in rate and reduction in streak-break rate are computed with experiment_id attribution and displayed with confidence intervals. Given sufficient sample size (≥500 users per variant), when KPIs are computed, then they are available by 09:00 UTC next day with data freshness SLA ≤24 hours.
Remote Config, Feature Flags, and A/B Testing
Given remote config is reachable, when the app starts or after 60 minutes, then it fetches and caches config controlling layout variants, default reaction behavior, and prioritization rules; on failure, last-known-good is used. Given experiment bucketing, when a user_pseudo_id is evaluated, then variant assignment is deterministic and consistent across devices for that user, with experiment_id and variant attached to all events. Given a kill-switch flag is enabled server-side, when clients sync config, then widget/complication interactions are disabled within 10 minutes and non-essential events cease. Given targeting rules, when eligibility predicates are not met, then users default to the control variant and no experiment_id is attached.
Consent, Privacy, and Opt-Out Controls
Given first use of the Room Carousel, when telemetry consent is required by locale, then a consent prompt is shown before any non-essential telemetry is sent; opt-in is explicit. Given a user opts out of analytics, when the setting is changed, then new telemetry and experimentation identifiers stop within 5 minutes, queued non-essential events are purged, and the user is assigned to control/no-experiment. Given event payloads, when they are constructed, then they exclude PII and include only pseudonymous identifiers, and all data is encrypted in transit. Given an account deletion request, when processed, then analytics identifiers are removed or anonymized within 30 days, and future events are not associated to prior identifiers.

Quick React Strip

Send a like, boost, or emote straight from the widget/complication with a single tap. Provide motivating social proof in seconds—no typing, no app open—keeping room energy high while you stay focused.

Requirements

One-tap Widget/Complication Reaction
"As a focused remote worker, I want to send a quick reaction from my widget with one tap so that I can support my room without breaking flow."
Description

Provide a tappable reaction strip (like, boost, emote) directly on mobile widgets and watch complications that sends a reaction to the selected room without launching the app. Enable single-tap interactions from the home/lock screen and wearable surfaces where supported, with sub-300ms perceived response via optimistic UI. Integrate with room activity feeds, increment engagement metrics, and respect system states (e.g., Do Not Disturb). Ensure minimal friction to maintain habit momentum and keep room energy high while users remain focused on their primary task.

Acceptance Criteria
One-tap reaction from mobile widget without app launch
Given the user is authenticated and has configured the widget to a room And the device surface supports tappable widgets When the user taps Like, Boost, or Emote on the widget Then the app does not launch or foreground And an optimistic visual confirmation appears within 300ms And the reaction is sent to the backend without requiring additional input And on success the confirmation state persists for at least 1s And on failure an error indicator appears within 2s and the UI reverts to pre-tap state
One-tap reaction from watch complication with optimistic UI
Given the complication is added and linked to the user’s account and a room When the user taps the complication reaction Then no app view opens on the watch or phone And a light haptic and visual confirmation occur within 300ms And the reaction is transmitted to the backend when connectivity is available And on transmission failure a non-intrusive error state is shown within 2s
Reaction types (Like, Boost, Emote) render and post correctly
Given the reaction strip displays Like, Boost, and Emote options When the user taps Like Then a Like reaction is posted for the configured room When the user taps Boost Then a Boost reaction is posted for the configured room When the user taps Emote Then the last-used or default emote is posted for the configured room And the selected icon shows a sent state for at least 1s
Room targeting from configured widget/complication
Given the widget/complication is configured to room R and the user is authenticated When the user sends a reaction Then the reaction is attributed to room R under the user’s identity with a UTC timestamp And no reaction is posted to any other room When no room is configured Then tapping shows a non-disruptive inline prompt to configure a room and no network request is sent When the user session is expired Then a non-disruptive inline sign-in required indicator is shown and the app is not auto-launched
Offline queueing, retry, and idempotency
Given the device is offline or the backend is temporarily unreachable When the user taps a reaction Then the UI shows a queued state within 300ms and does not open the app And the reaction is enqueued locally with timestamp and type And duplicate taps within 1s are deduplicated to a single queued item When connectivity returns Then the queued reaction auto-sends within 60s And up to 3 retries with exponential backoff occur over a maximum of 5 minutes And on success the UI updates to success and the queue entry is cleared And on final failure the UI shows a failure state and no further retries occur
Respect Do Not Disturb / Focus modes
Given the device is in Do Not Disturb or Focus mode When the user taps a reaction on the widget/complication Then no sounds are played and no intrusive system prompts are shown And only subtle haptic (or none where required by system settings) is used And no self-notification is generated for the actor as a push alert And the action completes or queues without breaking the active system mode
Metrics increment and activity feed integration
Given a reaction is successfully delivered to the backend Then the room’s engagement metrics are updated: reaction_count += 1 and unique_reactors for the day reflects the user And an activity feed entry appears for all room members within 5s including user id, reaction type, timestamp, and source=widget|complication And rapid repeated taps within 1s do not create duplicate feed entries or metric increments (idempotent) And analytics events include platform (iOS/Android/Watch), surface (widget/complication), and latency buckets
Reaction Strip Customization
"As a creator running multiple rooms, I want to customize which reactions show on my widget so that the options match the vibe and needs of each room."
Description

Allow users to configure which reactions appear in the Quick React Strip per room and globally, including order and default intensity (e.g., boost levels). Provide a simple in-app editor with presets and a long-press gesture on the widget/complication to swap or reorder reactions where OS allows. Persist settings across devices via user profile sync. Defaults should be sensible for new users, with the ability to restore to default. Configuration changes must immediately reflect across all widget surfaces.

Acceptance Criteria
Global and Room Reaction Strip Configuration Editor
Given I am in the in-app Reaction Strip editor for a room, When I reorder reactions, add or remove reactions, and set default intensities, And I tap Save, Then that room’s Quick React Strip shows the updated reactions in the saved order, with the saved default intensities, on the room view and its widgets/complications. Given I am in the global Reaction Strip editor, When I change reactions, order, and default intensities and Save, Then all rooms without an override inherit the new global configuration across app and widgets. Given I apply a provided preset in the editor, When I Save, Then reactions, order, and default intensities are updated to the preset values for the chosen scope (global or room). Given a reaction is removed in the editor for a scope, When I Save, Then that reaction no longer appears in the Quick React Strip for that scope.
Widget Long-Press Reorder/Swap (OS Permitting)
Given my device OS allows widget/complication customization via long-press, When I long-press the Quick React Strip and drag to reorder or tap to swap reactions, Then the new order/selection is saved for the appropriate scope (room or global as applicable) and is reflected in the app editor. Given my device OS does not allow widget/complication customization via long-press, When I long-press the widget, Then no inline reorder/swap UI is shown and the app remains stable without errors.
Default Reaction Intensities Applied via One-Tap
Given a reaction supports intensity levels, When I set its default intensity in the editor and Save, Then a single tap on that reaction in the Quick React Strip sends the reaction at the saved default intensity. Given a reaction does not support intensity levels, Then the default intensity control is hidden or disabled for that reaction in the editor.
Immediate Local Widget Update on Configuration Change
Given I Save changes to reactions, order, or default intensities for a scope on a device, Then all Quick React Strip instances on that same device (in-app surfaces, widgets, and complications) display the updated configuration within 2 seconds without requiring an app restart. Given multiple widget/complication instances exist for the same scope, When configuration changes are saved, Then all instances show identical reactions and order within 2 seconds.
Cross-Device Sync of Reaction Strip Settings
Given I am signed in on multiple devices, When I Save configuration changes on Device A, Then Devices B..N reflect the new configuration on app and widgets within 60 seconds while online. Given Device B is offline when changes are made on Device A, When Device B reconnects and profile sync completes, Then the new configuration appears on Device B’s app and widgets within 60 seconds of reconnection.
New User Sensible Defaults
Given I am a new user with no prior configuration, When I first open a room or add the Quick React widget/complication, Then the strip is pre-populated with a non-empty default set of reactions in a sensible order with usable default intensities. Given I am a new user, When I open the global editor before making any changes, Then the default preset is selected and visible.
Restore Defaults for Global and Room Configurations
Given I am in the global editor, When I tap Restore Defaults and confirm, Then the global Quick React Strip resets to shipped defaults (reactions, order, default intensities), and all rooms without overrides adopt these defaults across app and widgets. Given I am in a room editor with an override, When I tap Restore Defaults for that room and confirm, Then the room reverts to inherit from the global configuration and matches the current global defaults. Given defaults are restored, Then all local widgets update within 2 seconds and other devices update within 60 seconds.
Smart Room Targeting
"As a daily habit tracker, I want my widget to send reactions to the right room by default so that I don’t need to micromanage targeting each time."
Description

Automatically target reactions to the user’s most relevant room based on recent activity (e.g., last active room, current live session, pinned default). Provide an accessible mechanism to switch target rooms from the widget (e.g., long-press to cycle or choose from a short list) and clear visual labeling of the current target. Honor user preferences for default room per device. Fall back gracefully when no room is eligible by prompting to open the app.

Acceptance Criteria
Target Priority: Live Session > Last Active (24h) > Device Default
Rule 1: If the user is currently joined to a live room session (session status = active for user), set target = that room. Rule 2: Otherwise, if the user has interacted (check-in or reaction) in any room within the past 24 hours, set target = the room with the most recent interaction timestamp. Rule 3: Otherwise, if a per-device default room is set, set target = that default room. Rule 4: Otherwise, no eligible target exists and fallback must be invoked. Rule 5: Ties between candidate rooms are resolved by the latest user interaction timestamp. Rule 6: Ineligible rooms (user not a member, archived, or deleted) are excluded from consideration at all times. Rule 7: On the next widget interaction, the target used must match these rules 100% of the time.
Widget Long‑Press Room Switcher
Given the quick react widget is visible When the user long‑presses the widget Then a room picker appears listing up to 5 eligible rooms ordered: active live sessions first (most recent start first), then most recent interaction rooms, including the per‑device default if not already listed And each item displays room name and avatar And when the user taps a room in the list Then that room becomes the target within 1 second and persists for subsequent quick taps until changed And the widget label updates to the selected room And screen reader announces "Target set to {room name}" And if no eligible rooms exist, the picker shows "No rooms available" and offers an "Open app" action.
Clear Target Label on Widget/Complication
Rule 1: The widget/complication displays the current target room display name (not ID) and room avatar/badge. Rule 2: If the name exceeds available width, it truncates with an ellipsis without clipping other UI. Rule 3: The label updates within 1 second after the target changes due to priority rules or user selection. Rule 4: The accessible name reads "Quick react target: {room name}" and is focusable. Rule 5: Text and icon meet WCAG AA contrast (≥ 4.5:1 for text) in light and dark modes. Rule 6: If the target is the device default (and not live/recent), a subtle "Default" indicator is shown.
Per‑Device Default Preference Honored
Given device A has default room = X and device B has default room = Y And the user has no current live session and no interaction in the past 24 hours When the user taps quick react on device A Then the reaction routes to room X And when the user taps quick react on device B Then the reaction routes to room Y And changing the default on device A does not alter device B And removing the default on a device causes targeting to follow priority rules on that device.
No Eligible Room Fallback Prompt
Given there is no current live session, no interaction in the past 24 hours, and no per‑device default When the user taps the quick react widget Then no reaction is sent And a prompt is shown stating "Choose a room in StreakShare" And an action "Open app" is provided And tapping the action opens StreakShare to the room selection view within 2 seconds And dismissing the prompt performs no other side effects.
Eligibility Filters and Safeguards
Rule 1: Rooms where membership != member or status ∈ {archived, deleted} are excluded from target computation and switcher lists. Rule 2: If the selected target becomes ineligible before a tap is processed, the tap does not send and the fallback prompt is shown instead. Rule 3: 0% of reactions are posted to an ineligible or wrong room across 100 consecutive automated test taps. Rule 4: Network or server errors while sending do not change the current target selection; the user may retry without retargeting.
Recency Window and Deterministic Selection
Given the user reacts in Room A at T0 And the user reacts in Room B at T0 + 10 minutes And there is no current live session When the user quick reacts at T0 + 11 minutes Then the target is Room B And when the user quick reacts at T0 + 25 hours (with no device default) Then no eligible target exists and the fallback prompt is shown And when the user quick reacts at T0 + 23 hours 59 minutes Then the target is the most recently interacted room within the 24‑hour window.
Real-time Delivery & Feedback
"As a participant keeping momentum, I want immediate confirmation that my reaction landed so that I feel my contribution and stay engaged."
Description

Deliver reactions to the backend in real time and reflect them instantly in the target room feed with optimistic acknowledgment on the widget/complication (e.g., brief checkmark, haptic tick, and updated count). Reconcile server confirmation to correct any optimistic state. Provide lightweight animations that do not disrupt device performance. Ensure feedback is accessible with clear labels and adequate contrast and supports haptics where available.

Acceptance Criteria
Single-Tap Reaction Sends and Shows Optimistic Checkmark
Given the user is authenticated and viewing the Quick React Strip on a widget or complication And network connectivity is available When the user taps a reaction (like, boost, or emote) Then the client sends a reaction payload with roomId, userId, reactionType, timestamp, and clientId to the backend within 150 ms And the widget displays a checkmark and increments the visible reaction count within 100 ms of the tap And a haptic tick is triggered where haptics are available And the optimistic state remains until server confirmation or failure is received
Server Confirmation Reconciles Optimistic State
Given a pending optimistic reaction identified by clientId When a success response is received from the server within 3 seconds Then mark the reaction as confirmed and keep the incremented count without duplication And transition the checkmark to a confirmed state within 100 ms of receipt When a failure (4xx/5xx) or duplicate rejection is received Then revert the optimistic count, show a brief error indicator on the widget for 2 seconds, and emit an analytics event including error code and latency And ensure idempotency by preventing duplicate counts for the same clientId across retries
Real-Time Room Feed Reflects Reaction
Given a reaction has been accepted by the backend When the target room feed is open on any active client Then the reaction item appears in the feed within 1 second via the real-time channel And the aggregate reaction count updates once without creating duplicate entries And feed ordering remains consistent using server timestamps
Offline and Retry Behavior for Widget Tap
Given the device is offline or the request times out after 3 seconds When the user taps a reaction on the widget or complication Then show a queued state (e.g., hollow checkmark) within 100 ms and do not commit the count permanently And queue the reaction for background retry with exponential backoff up to 3 attempts or until connectivity is restored within 30 seconds And on success, confirm and finalize the count; on final failure, clear the queued indicator, play an error haptic where available, and surface a non-intrusive error badge for 2 seconds
Performance and Animation Budget on Widget/Complication
Given the optimistic acknowledgment and confirmation animations are triggered When the animation plays Then total animation duration is 300 ms or less at 50 fps or greater And additional CPU usage remains below 10% for under 500 ms and memory overhead remains below 10 MB during the interaction And no UI thread frame exceeds 16 ms for more than one consecutive frame And the widget or complication remains responsive to further taps within 150 ms after animation completes
Accessible Feedback: Labels, Contrast, Haptics, and Reduced Motion
Given system accessibility settings are enabled or disabled When the user focuses or taps any reaction control Then each control exposes an accessible name and hint (e.g., "Send like to <room name>") and supports VoiceOver/TalkBack activation And icon and text affordances meet WCAG 2.1 AA contrast (icons >= 3:1; text < 18 pt >= 4.5:1; text >= 18 pt >= 3:1) And haptic feedback is provided where supported and respects system haptics settings And when Reduce Motion is enabled, replace motion animations with a 100 ms fade and no translation or scale effects
Secure Background Execution
"As a privacy-conscious user, I want reactions sent from widgets to be secure so that my account and rooms are protected from misuse."
Description

Execute widget/complication actions via secure, scoped tokens that allow background API calls without fully launching the app. Implement short-lived tokens, refresh logic, and rate limiting to prevent abuse. Restrict payload to reaction type and room ID, and encrypt transport. Respect platform privacy requirements and do not expose sensitive data on the widget. Log and audit widget-originated actions for moderation and support.

Acceptance Criteria
Widget Reaction via Scoped Token (Background)
Given a logged-in user has installed the Quick React Strip widget and holds a valid scoped widget token When the user taps a reaction (like/boost/emote) on the widget Then the request is executed in the background without launching or foregrounding the host app process And the API call is authenticated with a scoped token limited to action:react and the target roomId And the API responds with HTTP 2xx and the reaction is recorded for the correct user and roomId And no additional permissions are requested at tap time and no sensitive data is rendered on the widget
Token Expiry, Rotation, and Background Refresh
Given widget tokens are short-lived with a TTL ≤ 10 minutes and refresh threshold at ≤ 60 seconds remaining When the token is expired or within the refresh threshold at tap time Then the widget performs a background token refresh using a refresh mechanism that does not foreground the app And on successful refresh, the reaction request proceeds with the new token and succeeds with HTTP 2xx And on refresh failure, the reaction request is not sent, an error is returned (HTTP 401/403), and no sensitive data is exposed on the widget And the previous token is revoked or unusable after rotation
Rate Limiting for Widget-Originated Reactions
Given server-side rate limits of max 3 widget reactions per user per room per 10 seconds and max 20 widget reactions per user across all rooms per minute When the widget attempts requests that exceed either threshold Then the server responds with HTTP 429 and a structured error code (rate_limited) And no additional reactions are recorded beyond the allowed limits And the client suppresses further widget reaction attempts for 30 seconds (cooldown) without foregrounding the app And all 429 responses are logged for monitoring with no token values recorded
Minimal Payload and Transport Security
Given a widget-originated reaction request When the request body is constructed Then only reactionType, roomId, and requestId are included in the JSON payload, with no PII or extraneous fields And Authorization uses a short-lived scoped bearer token bound to the user and (optionally) roomId scope And transport uses TLS 1.2+ with certificate pinning; on handshake/pin failure the request is aborted and not retried over insecure channels And cookies, persistent identifiers, and query params containing sensitive data are not used
Privacy-Safe Widget Surface and Secure Storage
Given platform privacy requirements for widgets/complications When the widget renders and handles reactions Then no sensitive data (e.g., email, full name, access token, room invite links) is displayed or logged by the widget/extension And widget tokens are stored only in OS secure storage (Keychain/Keystore) with hardware-backed protection where available and are not logged or written to disk in plaintext And access is limited to the app and its widget/extension using a restricted access group; tokens are not exported to backups And no additional runtime permissions (e.g., contacts, location) are requested to send reactions
Audit Logging and Moderation Traceability
Given a widget-originated reaction succeeds or fails When the server processes the request Then an audit record is written containing: userId, roomId, reactionType, timestamp (UTC ISO-8601), requestId, origin=widget, result (success/failure), and rateLimitBucket where applicable And audit records are queryable by userId, roomId, requestId within 60 seconds of the event and retained for ≥ 90 days And tokens or secrets are not stored in audit logs; only tokenId or scope identifiers may appear And support staff can retrieve a single reaction’s audit trail end-to-end using requestId
Replay Protection and Idempotent Submission
Given each widget reaction includes a unique requestId (UUIDv4) and token-bound nonce When duplicate requests with the same requestId are received within a 60-second idempotency window Then the server returns HTTP 200 with idempotent=true and does not create duplicate reactions or increment counts And requests with timestamps outside a ±2-minute skew window are rejected with HTTP 401/403 (stale/replay) And the token cannot be reused across devices; requests must include a device-bound claim validated by the server
Offline Queue & Retry
"As a commuter with spotty signal, I want my reactions to send automatically when I’m back online so that I don’t lose my support streak."
Description

When the device is offline or connectivity is poor, queue reactions locally from the widget/complication and retry with exponential backoff once a connection is available. Provide subtle, non-intrusive status feedback (e.g., queued indicator) and reconcile with the server to avoid duplicates. Store only minimal metadata and purge successfully delivered events promptly to conserve resources.

Acceptance Criteria
Queue Reaction From Widget While Offline
Given the device has no internet connectivity and the user taps a reaction from the Quick React Strip widget/complication When the tap is processed Then a reaction event is created locally with an idempotency key and appended to the offline queue And no network call is attempted during this state And the user can immediately perform additional taps without delay And a subtle queued indicator appears within 300 ms and auto-dismisses within 1.5 s And the app remains closed and in the background
Exponential Backoff And Connectivity Retry
Given one or more reaction events are queued and network connectivity becomes available When the retry worker runs Then events are sent in FIFO order with exponential backoff delays of approximately 1s, 2s, 4s, 8s, doubling up to a maximum interval of 60s per event And on HTTP 2xx the event is marked delivered and removed from the queue within 2s And on transient errors (timeouts, DNS failures, HTTP 429/5xx) the event remains queued and the backoff attempt count increments without user-facing interruption And on non-retryable errors (HTTP 400/403/404) the event is dropped and a silent failure is recorded for telemetry And total retry duration per event does not exceed 15 minutes
Idempotent Delivery And Duplicate Prevention
Given the user taps the same reaction multiple times while offline or during flaky connectivity When queued events are delivered with their idempotency keys Then the server accepts at most one instance per idempotency key And the client treats success or duplicate-acknowledged responses as delivered And exactly one reaction appears in the room activity stream per user tap And any redundant local duplicates are purged from the queue
Minimal Metadata Storage Constraints
Given reactions are queued locally Then each queued item stores only: reactionType, roomId, userId reference, createdAt timestamp, source (widget/complication), and an idempotency key And no message text, media, or additional PII is stored And the serialized size per queued item does not exceed 512 bytes And the queue is persisted to disk using OS-provided secure storage
Post-Delivery Purge And Resource Conservation
Given an event is acknowledged by the server When the acknowledgement is received Then the event is removed from the local in-memory queue and on-disk storage within 2 seconds And any temporary UI indicators related to the event are cleared And no residual files or records remain after app restart And delivery telemetry records tap-to-ack latency
Non-Intrusive Status Feedback In Widget
Given the user taps a reaction from the widget/complication while offline or under retry When the event is queued and later delivered Then a compact visual status appears in the widget (e.g., badge/dot) without opening the app or stealing focus And no full-screen notifications or blocking toasts are shown And haptic feedback, if enabled, is limited to a single light tap per user action And the status updates to delivered within 300 ms of receiving server acknowledgement, or quietly disappears if offline persists beyond 2 seconds
Queue Persistence Across App/Device Restart
Given there are queued reactions and the app process or device restarts before delivery When the system restarts and connectivity becomes available Then the queue is restored without data loss And retries resume with preserved backoff state per event And no duplicate reactions are sent due to restart And status indicators reflect the current state without requiring the user to open the app
Cross-platform Parity & Performance
"As a user who switches devices, I want the same quick reaction experience everywhere so that my routine stays consistent."
Description

Ship the Quick React Strip on iOS (Home/Lock Screen interactive widgets), Android (Home Screen widgets and Quick Settings tile), and wearables (Apple Watch and Wear OS complications/tiles) with consistent behavior and visual language adapted to platform guidelines. Optimize for low latency, battery impact, and minimal memory footprint. Establish a shared design system and analytics taxonomy to compare performance and engagement across platforms.

Acceptance Criteria
iOS Interactive Widgets: One-Tap Quick React
- Given an authenticated user on iOS 17+ with a StreakShare interactive widget on Home or Lock Screen, when the user taps Like, Boost, or Emote, then the reaction is sent without launching the full app UI. - Visual acknowledgement (pressed state + check/haptic) appears within 200 ms of tap. - Server receives the reaction within 1.5 s at p95 over Wi‑Fi/LTE; queued with retry if offline and auto-sent within 10 s of connectivity returning. - No duplicate reactions for a single tap; multiple rapid taps are debounced to max 1 reaction per 500 ms per control. - Widget reflects disabled state if the room session is inactive or user lacks permission; no network call is made in disabled state.
Android Widget & Quick Settings Tile Parity
- Home Screen widget and Quick Settings tile expose Like, Boost, Emote in the same order and iconography as iOS, adapted to Material guidelines. - Tapping any control posts the reaction without opening a full-screen Activity; a lightweight acknowledgement shows within 300 ms. - Reaction delivery p95 <= 1.5 s on Wi‑Fi/LTE; offline taps are queued via WorkManager (expedited when allowed) and auto-sent within 10 s of connectivity return. - Crash-free rate >= 99.9% and ANR rate == 0 across 1,000 automated taps per surface. - Tile/widget respect dark mode and RTL; minimum touch target >= 48x48 dp.
Wearables: Apple Watch & Wear OS One-Tap Reaction
- From Apple Watch complication/Smart Stack widget or Wear OS tile, a single tap on Like, Boost, or Emote sends a reaction; if platform requires foregrounding, no additional user action is required. - Haptic feedback on tap within 150 ms and visual confirmation within 1 s. - p95 reaction delivery <= 2.0 s over Bluetooth/Wi‑Fi/LTE; offline taps are queued and synced within 15 s of connectivity restoring to paired phone/cloud. - Energy/CPU budget: per-tap CPU time <= 200 ms and no foreground session > 2 s; no persistent wake locks. - Minimum touch target >= 40x40 pt (watchOS) / 40x40 dp (Wear OS); respects round/square screens with no clipping.
Design System Tokens & Visual Parity Across Platforms
- A shared token set defines sizes, spacing, color roles, typography, states (idle/pressed/disabled) and is mapped to SwiftUI/WidgetKit, Jetpack Compose/Glance, and watch frameworks. - Iconography and control order are consistent: Like | Boost | Emote; platform adaptations use SF Symbols on iOS/watchOS and Material icons on Android/Wear OS. - Visual regression tests/snapshots pass on all surfaces with <= 2 px/pt allowable variance and correct dark/light theming. - Touch targets meet platform minimums: iOS >= 44x44 pt, Android >= 48x48 dp, watchOS/Wear OS >= 40x40. - Color contrast for foreground/background and pressed states meets WCAG AA (>= 4.5:1 where applicable).
Analytics Taxonomy & Cross-Platform Comparability
- Implement events with identical names and required properties across platforms: quick_react_tap, quick_react_sent, quick_react_fail. - Required properties: platform, surface (home_widget|lock_widget|qs_tile|watch_complication|wear_tile), reaction_type (like|boost|emote), room_id, user_id_hash, latency_ms, offline_queued (bool), app_version, schema_version. - 100% of successful sends emit quick_react_sent; 100% of errors emit quick_react_fail with error_code. - In synthetic tests of 1,000 taps per platform, tap->sent funnel rates match within ±5% and median latency within ±10%. - No PII beyond hashed IDs is logged; user analytics opt-out is respected and suppresses all events.
Performance Budgets: Latency, Battery, Memory
- Tap-to-local acknowledgement p95 <= 300 ms across all surfaces. - Tap-to-server-acknowledgement p95 <= 1.5 s on phones and <= 2.0 s on wearables over Wi‑Fi/LTE. - Memory: extension/process RSS increase <= 10 MB (iOS widgets) and <= 20 MB (Android widget/tile) during operation. - Battery/energy: classified as Low in platform profiling; no more than 1 background wakeup per tap; background work completes p95 <= 5 s. - Reliability: duplicate send rate < 0.1%, dropped reaction rate < 0.5% over 10,000 taps in load tests.

SafeTap Undo

Built-in, 5-second on-widget undo with gentle haptics after any lockscreen check-in or reaction. Prevents accidental taps and anxiety while preserving the speed of one-tap actions.

Requirements

5-Second Tap Buffer & Deferred Commit
"As a daily habit tracker using the lock screen, I want a brief window after I tap to check in so that I can undo accidental taps without slowing down my normal one-tap flow."
Description

Implements a post-tap buffer that defers final commit of any lock-screen check-in or reaction for 5 seconds while displaying an optimistic state to the user. The action is queued client-side with a unique operation ID and held server-side until the buffer elapses or an undo is received. Supports multiple concurrent actions per user across rooms, ensures idempotency and de-duplication, and prevents fan-out (streak updates, notifications, reactions broadcast) until commit. If the app/widget is killed or the device sleeps, the backend defaults to committing after the window, preserving intent while enabling reversal if the undo is tapped. Guarantees consistent streak logic by resolving the final state before streak validation and decay logic runs. Designed to add zero perceptible latency to initial tap feedback while providing deterministic rollback within the window.

Acceptance Criteria
Optimistic Lock-Screen Check-In With 5-Second Deferred Commit
Given a user taps a lock-screen check-in When the tap is received by the widget Then the UI reflects an optimistic checked-in state within 100 ms and delivers gentle haptics within 50 ms And the client generates a unique operationId and queues the action locally And the backend stages the action with the same operationId and defers commit for 5.0 ±0.2 seconds (server time) And no streak update, notification, or reaction broadcast occurs before commit And upon buffer expiry without undo, the backend commits exactly once and emits fan-out And the client reconciles the committed state within 500 ms of commit
In-Window Undo Cancels Queued Action
Given a staged action is within its 5-second buffer window When the user taps Undo within the window Then the backend marks the operation as canceled and drops the commit And the optimistic UI fully reverts within 150 ms and plays gentle haptics And no streak update, notification, or reaction broadcast is emitted And multiple Undo taps or duplicate cancel requests are handled idempotently And the client receives confirmation of cancellation within 500 ms
Concurrent Actions Across Multiple Rooms
Given a user initiates actions in multiple rooms within a short interval When two or more actions are staged concurrently Then each action is assigned a distinct operationId and maintains an independent 5-second buffer And commits and undos are isolated per operation and do not interfere across rooms And the system supports at least 10 concurrent staged actions per user without dropped commits or undos And each action’s finalization (commit or cancel) is correctly applied to its originating room only
Process Kill or Device Sleep During Buffer Window
Given an action is staged and the client app/widget process is killed or the device sleeps before the window ends When no Undo is received before the buffer elapses Then the backend auto-commits the action at T0 + 5.0 seconds using server time authority And the action’s fan-out (streak update, notifications, reaction broadcast) is emitted exactly once after commit And upon next client session, the client reconciles to the committed state within 1 second And any Undo attempt after window expiry is rejected with a clear reason and no state change
Idempotency and De-duplication Under Retry
Given unreliable networks may cause request retries or duplicates When the backend receives multiple requests with the same operationId Then the operation is processed exactly once and subsequent duplicates are acknowledged without side effects And fan-out is emitted at most once per committed operation And telemetry records a single committed event per operationId And if a different operationId is submitted for a separate action, it is treated independently subject to business rules
Streak Validation and Fan-Out Gated by Final State
Given streak validation and decay jobs execute on a schedule When staged operations exist for a user and room Then the system resolves each operation to a final state (commit or cancel) before running streak logic And streak increments or decay prevention occur only for committed actions And no notifications, feeds, or broadcasts are emitted until after commit And downstream consumers observe the commit event before any derived events, preserving causal order
On-Widget Undo UI & Countdown
"As a user making quick check-ins, I want an immediate, obvious undo button with a short countdown on the widget so that I can confidently reverse mistakes without navigating into the app."
Description

Adds an inline, non-intrusive undo affordance on the lock-screen widget immediately after a tap, featuring a clear ‘Undo’ CTA and a visible 5-second countdown (progress ring or subtle timer). The component anchors near the original tap target, respects safe areas, dark/light themes, and reduces overlap with other widget controls. Interaction is single-tap undo with generous hit targets; the control persists only for the buffer duration and then dismisses silently. Visual states cover optimistic ‘posted’, ‘undo pending’, and final ‘committed’ with smooth transitions. Designed for low overdraw and battery impact, and adaptable to platform widget constraints to ensure consistent behavior across supported lock-screen surfaces.

Acceptance Criteria
Post-tap Undo Affordance Appears with 5s Countdown
Given the StreakShare lock-screen widget is visible and the user taps a check-in or reaction When the tap is registered Then an inline Undo control appears within 150 ms, anchored near the original tap target And a visible 5-second countdown is shown (progress ring or numeric timer) And the Undo control persists for 5.0 ±0.1 seconds unless tapped And the initial visual state reflects "posted" optimistically
Single-Tap Undo Cancels Optimistic Action
Given an Undo control is visible for a just-posted check-in or reaction When the user single-taps the Undo control Then the pending action is canceled locally within 150 ms and the UI reverts to the pre-tap state And any in-flight network request is canceled, or a compensating delete is sent within 1 second And a gentle confirmation haptic is played once And no streak/reaction increment remains in the UI or server after sync completes
Silent Auto-Commit After Countdown Expiry
Given an Undo control is visible for a just-posted action When the 5-second countdown elapses without an Undo tap Then the action is committed and any queued network post is allowed to complete And the Undo control dismisses silently with no banner, toast, or sound And the final "committed" visual state transitions smoothly (≤300 ms) to idle
Anchoring, Safe Areas, and Non-Overlap
Given the widget is displayed on any supported lock-screen surface (Lock Screen, StandBy) in portrait or landscape When the Undo control appears Then it is rendered within the widget safe area and does not overlap the hit targets of other widget controls And it maintains at least 8 pt spacing from the nearest interactive element And its tappable hit target is at least 44x44 pt And if the preferred anchor would violate constraints, an alternate position is selected that satisfies all constraints
Theming, Accessibility, and Haptics Compliance
Given system theme (light/dark) and accessibility services (VoiceOver) are enabled or disabled When the Undo control appears Then all text/icons meet a minimum contrast ratio of 4.5:1 against the background And the control is labeled for accessibility as "Undo check-in" or "Undo reaction" and is focusable and actionable with a single activation And a gentle haptic is played on appearance and on successful undo; if haptics are unavailable, no error occurs and visuals remain sufficient And dynamic text size does not cause truncation of the "Undo" CTA or timer; ellipsize or icon-only fallback is applied while preserving the 44x44 pt hit target
Performance and Battery Constraints on Lock Screen
Given the Undo control is displayed for 5 seconds with its progress animation When measured on representative devices using system profiling tools Then average CPU utilization attributable to the widget remains ≤5% during the buffer window (peak ≤10%) And average frame render time for the progress animation is ≤16 ms with zero dropped frames And GPU overdraw for the widget view is ≤2x on average And no timers, animations, or network polling continue beyond 100 ms after the control is dismissed
Visual States and Transitions Integrity
Given a user posts a check-in or reaction When the Undo control lifecycle runs Then states transition in order with smooth animations: "posted" -> "undo pending" -> "committed" (or revert on undo) And each state change animation completes in ≤200 ms with no flicker, jumps, or layout shifts And the component minimizes overdraw and avoids layout thrashing (no more than one layout pass per state change) And the control auto-dismisses within 100 ms of reaching the final state
Gentle Haptic Feedback & State Signals
"As a user checking in from my lock screen, I want gentle haptic cues confirming my action or undo so that I get immediate reassurance without disruptive vibrations."
Description

Defines platform-specific haptic patterns for post-tap confirmation and undo acknowledgement that are subtle, consistent, and respectful of system settings. Provides a light confirmation tick on initial action and a distinct, equally gentle cue on successful undo. Includes fallbacks for devices without advanced haptic engines and honors Do Not Disturb, accessibility vibration settings, and power-saving modes. Debounces repeated taps to avoid haptic spam and aligns timing with visual state changes for a cohesive, reassuring experience.

Acceptance Criteria
Lockscreen one-tap confirmation haptic
Given the device supports haptics and OS policy allows them And the StreakShare lockscreen widget is visible When the user performs a valid one-tap check-in or reaction Then a single light haptic tick is emitted within 50 ms of the visual confirmation state appearing And the tick duration is between 10 ms and 25 ms And no confirmation haptic is emitted if the tap does not change state
Undo success distinct haptic cue
Given the 5-second undo affordance is active after a one-tap action And the device supports haptics and OS policy allows them When the user taps Undo and the action is successfully reverted Then a distinct gentle double-pulse haptic is emitted within 50 ms of the visual state reverting And the pattern consists of two pulses of 8–20 ms each, separated by 60–100 ms And no undo haptic is emitted if the undo window has expired or the revert fails
Honor system, Do Not Disturb, and accessibility settings
Rule: If OS-level haptics/vibration is disabled or Do Not Disturb suppresses haptics, no haptic is emitted for confirmation or undo. Rule: Haptic intensity scales with OS haptic/vibration intensity settings (e.g., Low/Medium/High); at Low, amplitude is reduced by ≥50% vs Standard. Rule: When accessibility settings to reduce/limit haptics are enabled, the minimal-intensity variant is used (≤30% of Standard amplitude) while maintaining the distinct single vs double pattern. Rule: Behavior matches lockscreen policies; if the OS disallows haptics on the lockscreen, no haptic is emitted.
Debounce and throttle haptic output
Rule: A maximum of one haptic is emitted per 300 ms window per widget instance. Rule: During the 5-second undo window, additional taps on the same action do not emit confirmation haptics unless they change state (e.g., successful Undo followed by a new check-in). Rule: Reaction spamming from the widget is rate-limited to at most 3 haptics per 2 seconds.
Fallbacks for limited or absent haptic hardware
Rule: On devices without advanced haptic engines but with a basic vibrator, confirmation uses a single 15–25 ms vibration and undo uses a double 10–15 ms vibration separated by 60–100 ms. Rule: On devices without any vibrator/haptic hardware, no haptic is attempted; the UI state changes proceed without error and without delayed interaction. Rule: The chosen fallback patterns remain within the "light" intensity category defined by the OS (when available).
Timing alignment with visual state changes
Rule: For both confirmation and undo, the absolute time delta between haptic onset and the corresponding visual state transition is ≤50 ms for at least 95% of 100 sampled interactions on supported devices. Rule: No haptic is emitted for undo countdown expiry, auto-reverts, or failed operations; haptics are limited to initial post-tap confirmation and successful undo acknowledgment.
Power-saving mode haptic modulation
Rule: When system power-saving/low-power mode is active, haptic amplitude is reduced by ≥30% compared to Standard while preserving pattern distinctness (single vs double pulse). Rule: While in power-saving mode, rate-limiting allows at most 2 haptics per 2 seconds per widget instance. Rule: On exiting power-saving mode, haptic intensity and rate limits revert to Standard on the next emitted haptic.
Accessibility & Localization Compliance
"As a user who relies on assistive technologies, I want the undo option to be readable, focusable, and understandable in my language so that I can avoid accidental check-ins without barriers."
Description

Ensures the undo control and countdown are fully accessible and internationalized. Provides screen reader labels and live region announcements (e.g., “Check-in posted. Undo available for 5 seconds”), large tappable targets, sufficient color contrast, and Dynamic Type scaling. Offers motion-reduced countdown alternatives for users with reduced motion enabled and supports RTL layouts. Localizes all strings, pluralization, and timer formats, and validates behavior with VoiceOver/TalkBack to guarantee discoverability and operability for all users.

Acceptance Criteria
Screen Reader Announcement and Operability After Check-In
Given the user performs a lock screen check-in with a screen reader enabled When the check-in is posted Then a polite announcement of “Check-in posted. Undo available for 5 seconds” is spoken within 1 second And accessibility focus moves to the Undo control And the Undo control has the accessible name “Undo check-in” and a hint indicating the 5-second limit And activating Undo via the screen reader action reverts the check-in and announces “Check-in undone” within 1 second And if 5 seconds elapse without activation, announce “Undo expired” and remove the Undo control from the accessibility focus order And no more than two announcements occur during the 5-second window (initial and expiry)
Touch Target Size and Hit Area Compliance
Given the lock screen widget shows the Undo control after a check-in or reaction When measured on supported devices Then the Undo control’s touch target is at least 44x44pt on iOS and 48x48dp on Android And there is at least 6pt/dp of hit slop around the control where possible And adjacent controls maintain at least 8pt/dp spacing to prevent accidental activation And the control remains fully tappable at screen edges and on compact devices And no overlapping interactive regions are detected in hit-testing
Dynamic Type and Content Scaling on Lock Screen
Given the device text size is set from the smallest to the largest Dynamic Type/Font Size setting When the Undo control and countdown appear Then all text (labels, countdown value, hints) scales with system settings And no text is clipped, truncated without an accessible alternative, or overlaps other UI And the touch target remains at least 44x44pt iOS / 48x48dp Android at all sizes And the layout avoids horizontal scrolling and preserves widget boundaries And the control remains operable with one tap at all sizes
Color Contrast and State Visibility in Light/Dark Modes
Given system appearance is Light and Dark and the control is viewed against typical lock screen backgrounds When evaluating contrast Then text and iconography meet a minimum 4.5:1 contrast ratio against their backgrounds And non-text essential indicators (progress ring, focus outlines) meet at least 3:1 contrast And focus, pressed, disabled, and countdown states remain distinguishable in both modes And contrast is preserved for high-contrast accessibility modes where available
Reduced Motion and Haptics Respect System Settings
Given Reduce Motion (or Remove Animations) is enabled When the countdown appears Then no pulsing, progress sweep, or animated transitions play; a static or discretely updating indicator is shown And screen reader output is not spammed with per-second updates; only initial availability and expiry are announced And if system haptics/touch feedback is disabled, no haptic feedback is produced And any remaining transitions complete within 200ms without motion effects
Right-to-Left (RTL) Layout and Reading Order
Given the device locale uses a right-to-left script (e.g., Arabic, Hebrew) When the Undo control and countdown are displayed Then the layout mirrors horizontally, including icon placement and progress direction where appropriate And the semantic reading and focus order follow RTL expectations And numerals and punctuation in the countdown render per locale conventions And swipe gestures and affordances are directionally consistent with RTL
Localization, Pluralization, and Timer Formatting Coverage
Given the app is run in target locales (en, es, fr, de, pt-BR, ar, he, hi, ja, zh-Hans) When the Undo flow is triggered for check-ins and reactions Then all user-facing strings are localized with correct singular/plural forms (e.g., “1 second” vs “2 seconds”) And countdown numerals and time formats follow locale rules, including non-Latin digits where applicable And no string truncation or overflow occurs within the widget bounds across locales And pseudo-localization testing shows no hard-coded strings and UI remains usable And a fallback language is used if a translation is missing without breaking the flow
Offline Safety & Error Recovery
"As a user with spotty network, I want my tap or undo to resolve correctly even if I go offline right after tapping so that my streaks and reactions stay accurate."
Description

Builds a resilient client-server flow that preserves user intent during poor connectivity or app lifecycle interruptions. Queues actions with operation IDs, retries with exponential backoff, and guarantees at-most-once commit via idempotent endpoints. Supports undo while offline by marking the action for cancellation within the window and reconciling on reconnect, resolving conflicts deterministically if both commit and undo race. Handles rapid multi-taps and widget re-entry, prevents window extension abuse, and aligns final state with streak and reaction systems without duplication or gaps.

Acceptance Criteria
Offline Check-in Queued with Idempotent Commit
Given the device is offline and the user taps a check-in on the lockscreen widget When the client enqueues the action with a unique operationId and connectivity is restored Then the client sends the request with the operationId and removes it from the queue only after a 2xx/idiempotent success response And the server records the check-in at most once even if duplicate requests with the same operationId arrive And the user’s streak increases by exactly 1 for that day with no duplicate entries
Offline Undo Within Window Reconciles on Reconnect
Given the device is offline after a check-in and the user taps Undo within 5 seconds When connectivity is restored Then the client sends a cancel for the same operationId and the server final state is canceled And no streak increment is applied for that day and no reaction is recorded And the local UI transitions from pending to canceled synced without residual pending badges
Commit vs Undo Race Deterministic Resolution
Given a check-in and an Undo within 5 seconds are both sent due to flaky connectivity for the same operationId When the server receives both commit and cancel requests in any order Then resolution is deterministic: cancel wins if the undo timestamp is within 5 seconds of the original tap and the operationId matches; otherwise commit stands And all subsequent duplicate requests with that operationId return the same final state
Rapid Multi-taps and Widget Re-entry Deduplication
Given the user rapidly taps the check-in or reaction multiple times during the active 5-second undo window or re-enters the widget When the client processes the inputs Then only one operationId is maintained for that check-in event; extra taps within the active window are coalesced and do not create additional queued operations And no more than one check-in/reaction is committed server-side for that event
Undo Window Non-Extendable and Monotonic Timing
Given the user initiates a check-in and a 5-second undo countdown starts from the initial tap using a monotonic clock When the user backgrounds/foregrounds, re-enters the widget, toggles airplane mode, changes system time, or force-quits and relaunches during the window Then the undo countdown continues without extension; after exactly 5 seconds the undo action is disabled and any subsequent undo requests are rejected And the server ignores cancel requests for that operationId received after the 5-second window and preserves the commit
Final State Alignment with Streaks and Reactions
Given an operation results in a final state of committed or canceled after reconciliation When the client and server complete sync Then the user’s streak and reaction counts reflect the final state exactly once: committed increments/appends; canceled does not And historical records show exactly one operation with the operationId and a single final outcome And no gaps or duplicate days appear in the streak history
Exponential Backoff Retry Behavior and Persistence
Given a queued operation fails due to transient errors (network loss, 5xx, timeout) When the client retries Then retries use exponential backoff with jitter (initial delay ≈ 1s doubling each attempt with ±50% jitter) capped at 60s per attempt, and total retry time does not exceed 15 minutes And the queued operation and its undo window metadata persist across app relaunches And upon exceeding the total retry limit, the client stops retrying, marks the operation failed, and does not create duplicate operations
Privacy-Safe Undo Analytics & Alerts
"As a product owner, I want privacy-safe analytics on undo usage so that we can measure accidental tap reduction and iterate on the experience responsibly."
Description

Captures aggregate metrics on accidental taps and undos (rates, time-to-undo distribution, widget surface, device class) to validate efficacy and guide refinements. Excludes PII, adheres to consent settings, and samples where needed to minimize overhead. Feeds dashboards and anomaly alerts (e.g., spikes in undos after a UI change) and supports experimentation on countdown visuals or duration within allowed ranges to balance speed and confidence.

Acceptance Criteria
Consent-Gated Event Collection
Given analytics consent is disabled for the user When the user performs a lockscreen check-in, reaction, or an undo within the 5-second window Then no analytics events related to tap, undo, or timer countdown are queued or transmitted Given analytics consent is enabled for the user When the user triggers an undo within the 5-second window Then a single "undo" event is queued with required non-PII fields and transmitted on next flush Given a user toggles consent from enabled to disabled When the toggle is saved Then any unsent analytics in the local queue are purged within 10 seconds and no further events are collected Given the widget is used on a lockscreen surface When events are queued Then collection adheres to OS permissions and uses no platform identifiers beyond those permitted for analytics
PII Exclusion and Payload Whitelist
Given an analytics event is serialized client-side When validating the payload schema Then only the following fields are present: event_type, event_timestamp_rounded, widget_surface, device_class_coarse, time_to_undo_ms, sampled_flag, variant_id, app_version, os_major And no user identifiers (e.g., name, email, phone, exact device model string, ad ID, IP, precise location, contact lists) are present Given a payload contains any field not on the whitelist or matches a disallowed PII pattern When schema validation runs Then the event is dropped and a non-PII local warning is logged Given server-side ingestion is active When events arrive Then automated schema validation rejects any non-conforming payloads with a 4xx and logs aggregate counts without storing payload content
Adaptive Sampling and Performance Budget
Given remote configuration is available When sampling is updated Then per-event-type sampling rates can be set between 0% and 100% and take effect within 15 minutes Given a device is offline or the queue is full When new events are generated Then the queue caps at 50 KB and drops oldest events first without blocking the UI thread Given analytics collection is enabled at 100% sampling When the user performs typical daily usage (P50 active user) Then added app-start to first-interaction latency increases by ≤ 5 ms at P95, CPU overhead ≤ 1% avg, memory overhead ≤ 5 MB peak, and network egress ≤ 150 KB/day Given the app is under low-power or background restrictions When events are flushed Then flushes are deferred to OS-approved windows without waking the radio solely for analytics
Metrics Aggregation and Dashboard Availability
Given events are ingested When nightly aggregation runs Then dashboards display: accidental tap rate, undo rate, time-to-undo distribution (histogram with 0–5000 ms in 250 ms bins), segmented by widget_surface and device_class_coarse Given streaming aggregation is active When new events arrive Then alerting metrics are updated with end-to-end freshness P95 ≤ 15 minutes and standard dashboards P95 ≤ 60 minutes Given a new app version is released When filtering by app_version in the dashboard Then all core metrics render without errors and with ≥ 99% data completeness for opted-in users
Anomaly Detection and Alerting
Given a UI or configuration change is tagged with a deployment marker When the 30-minute rolling undo rate deviates > 50% above the 7-day same-hour baseline with z-score ≥ 3 for ≥ 10 minutes Then an anomaly alert is triggered and delivered to the on-call Slack channel and email within 5 minutes, including links to the relevant dashboard and diff Given multiple anomalies occur within a 30-minute window When alerts are generated Then alerts are deduplicated into a single incident with appended updates Given a maintenance window is scheduled When alerts would be generated in that window Then they are suppressed and a summary is sent after the window ends
Experimentation Guardrails for Undo Countdown
Given an experiment is created on countdown visuals or duration When variants are defined Then allowed duration values are within 3–7 seconds inclusive with control at 5 seconds, and only visual styles of the countdown are modified Given a user is not consented to analytics When bucketing occurs Then the user is excluded from the experiment and produces no experiment events Given the experiment is running When assignment happens Then users are stably bucketed by installation ID hash, sample ratio mismatch is monitored (χ² test p < 0.01), and metric readouts include accidental tap rate, undo rate, and time-to-undo distribution per variant Given the experiment reaches predefined stopping rules When significance is achieved or guardrails are violated (e.g., undo rate worsens by > 20%) Then the experiment auto-stops and reverts to control
Data Retention, Revocation, and Deletion
Given raw analytics events are stored When retention policies apply Then raw events are retained ≤ 30 days and aggregated metrics ≤ 180 days Given a user revokes consent or requests deletion When the request is received Then new data collection stops immediately and previously stored raw events associated with that user/device are purged within 24 hours; aggregates are recomputed or excluded where feasible without re-identification risk Given legal or compliance audits occur When retention and deletion logs are inspected Then immutable audit logs show timestamps and counts of purged records without containing PII
User Controls & Policy Constraints
"As a power user, I want limited control over the undo behavior and clear rules around when I can undo so that I can tailor speed versus safety without gaming the system."
Description

Provides a simple setting to enable/disable SafeTap Undo (default ON) and, if permitted, select a preset countdown duration within a narrow range while maintaining a consistent default of 5 seconds. Enforces policy constraints such as one undo per action, no window extension via repeated taps, and clear post-window states. Documents behavior in Help/FAQ and aligns with notification and streak policies to avoid confusion. Ensures enterprise/workspace configurations can lock defaults if required.

Acceptance Criteria
Default ON with User Toggle in Settings
Given a fresh install with no prior preference, When the user launches StreakShare, Then SafeTap Undo is enabled by default and the countdown is set to 5 seconds. Given SafeTap Undo is toggled OFF in Settings, When the user performs a lockscreen check-in or reaction, Then no undo affordance or haptic feedback is shown. Given SafeTap Undo is toggled ON in Settings, When the user performs a lockscreen check-in or reaction, Then the undo affordance appears and is usable for the configured duration. Given the user changes the SafeTap Undo toggle, When the app is restarted, Then the selected state persists on that device.
Preset Countdown Selection with 5s Default
Given the user has permission to edit SafeTap Undo duration, When they open Settings > SafeTap Undo > Duration, Then the only selectable presets are 3s, 5s, and 7s with 5s preselected by default. Given the user selects a duration preset, When they next perform a lockscreen check-in or reaction, Then the undo window length equals the selected preset (±0.1s tolerance). Given duration editing is not permitted, When the user views the Duration control, Then it is disabled and labeled as managed with the enforced value displayed.
Single Undo Limit per Action
Given a lockscreen check-in or reaction has been made and the undo window is active, When the user taps Undo within the window, Then the original action is reverted and no further undo is offered for that same action. Given the same action after an undo has been applied, When the user attempts any additional Undo on that action, Then no state change occurs and no undo affordance is shown. Given a finalized action (undo window expired), When the user attempts to access Undo, Then Undo is unavailable for that action.
Undo Window Not Extendable
Given an undo window begins at time t0 with duration D, When the user taps the widget or Undo affordance multiple times during the window, Then the window still expires at t0 + D with no extension. Given the user dismisses and reopens the app or associated notifications during the undo window, When the window is observed, Then its expiry remains at t0 + D with no extension. Given the undo window has expired, When the user taps any related UI, Then no undo action occurs and the action remains finalized.
Clear Post-Window Finalization States
Given an undo window expires without an undo, When the UI updates, Then the check-in/reaction remains, streak updates per policy, and the undo affordance disappears and is non-interactive. Given the user taps Undo within the window, When the UI updates, Then the check-in/reaction is removed, the streak does not increment/decay is unaffected, and any pending indicators are cleared. Given the post-window state is reached (undone or finalized), When the app is restarted, Then the final state persists and matches the last known outcome.
Notification and Streak Policy Alignment
Given a lockscreen check-in or reaction that would normally trigger notifications, When the undo window is active, Then emission of those notifications is deferred until the window ends. Given the user undoes the action within the window, When notification dispatch is evaluated, Then no notification is sent for that action and no duplicate is queued. Given streak counts and visible streaks, When the undo window is active, Then permanent streak updates are deferred and a pending state is shown; When the window ends without undo, Then the streak updates once; When undone, Then the streak remains unchanged.
Enterprise Lock of Defaults and Controls
Given a managed workspace policy enforces SafeTap Undo settings, When the user opens Settings, Then the SafeTap Undo toggle and Duration controls are disabled with a "Managed by your workspace" explanation and enforced values displayed. Given policy enforces ON with 5s, When the user performs a lockscreen check-in or reaction, Then the undo UI appears with a 5s window and cannot be changed by the user. Given policy enforces OFF, When the user performs a lockscreen check-in or reaction, Then no undo affordance is shown regardless of local preferences. Given the device leaves management or the policy is lifted, When the app is relaunched, Then the controls become editable and previously saved user preferences (if any) are restored.

Offline Queue

When you’re offline, lockscreen check-ins and reactions are locally time-stamped and queued, then auto-synced when you reconnect—preserving streak integrity on flights, subways, and spotty networks.

Requirements

Lockscreen Offline Capture
"As a remote worker on the subway, I want to check in from my lock screen even when I’m offline so that I preserve my streak with as little friction as possible."
Description

Enable capture of check-ins and reactions directly from the lock screen when the device has no connectivity. Implement lock screen widgets/notification actions on iOS and Android that write events locally without network calls, provide instant haptic/visual confirmation, and gracefully degrade to offline mode. Persist minimal event payload and context required for later sync, ensure zero-UI-blocking performance, and handle permission flows and OS constraints for background execution.

Acceptance Criteria
Lock Screen Offline Check-In Capture
Given the device has no connectivity (e.g., Airplane Mode enabled) and a lock screen widget/notification action for a habit is visible When the user taps the Check In action from the lock screen Then a single event is written locally with fields: event_id (UUIDv4), user_id, habit_id (or room_id), action_type=check_in, local_timestamp (UTC), timezone_offset_minutes, device_id, source=lock_screen, offline=true, os_version, app_build And the local write completes within 100 ms And haptic feedback is produced and a visual 'Queued' confirmation appears within 200 ms of the tap And no network request is initiated during the operation And the offline queue count increases by 1
Lock Screen Offline Reaction Capture
Given the device has no connectivity and a lock screen notification includes reaction actions for a specific target (from notification payload) When the user selects a reaction (e.g., emoji) from the lock screen action Then a reaction event is stored locally with fields: event_id (UUIDv4), user_id, target_id (from notification payload), reaction_code, action_type=reaction, local_timestamp (UTC), timezone_offset_minutes, device_id, source=lock_screen, offline=true, os_version, app_build And the local write completes within 100 ms And haptic feedback and a visual 'Queued' confirmation appear within 200 ms And no network request is initiated And repeat taps for the same target_id and reaction_code within 2 seconds result in a single queued event (deduplicated)
Queue Persistence, Ordering, and Capacity
Given there are N queued offline events (where 0 < N ≤ 500) When the app process is killed, the device reboots, or the app is relaunched Then all queued events remain persisted and recoverable And events retain a stable order by local_timestamp ascending, with stable tie-break by event_id And the queue capacity is 500 events per user; when the 501st event is added, the oldest event is pruned and a 'queue_pruned=true' flag is recorded for foreground surfacing And the per-event payload size does not exceed 1 KB on disk And persistence uses a transaction to ensure no partial/corrupted writes on crash
Performance and Non-Blocking Feedback
Given a mid-range device under typical load When the user triggers a lock screen Check In or Reaction while offline Then main-thread blocking time attributable to the action is ≤ 16 ms at p95 (≤ 50 ms at p99) And the local persistence completes ≤ 100 ms at p95 And haptic/visual confirmation appears ≤ 200 ms at p95 And no ANR or crash occurs during a stress test of 1,000 consecutive lock screen actions And the action performs zero network I/O and does not wake the cellular/Wi‑Fi radio
Graceful Degradation and Permissions
Given required permissions or OS capabilities for lock screen actions are missing or restricted (e.g., notifications disabled, widget not configured, background execution limited) When the user attempts a lock screen action Then the app does not crash and shows an inline message 'Unlock to enable' or 'Action unavailable' within the lock screen surface And tapping the provided affordance on unlock deep-links to the exact permission/configuration screen And once permissions are granted, the next lock screen attempt succeeds offline without requiring a full app relaunch And if the OS prevents execution entirely, no event is created, and the action visually indicates failure within 300 ms
Time, Deduplication, and Identity Integrity
Given the device local clock or timezone changes between actions while offline When multiple check-ins/reactions are captured before and after the change Then each event stores the original local_timestamp and timezone_offset_minutes reflecting the moment of tap And each event has a unique client_event_id (UUIDv4) used for idempotency during later sync And duplicate taps for the same (user_id, habit_id/target_id, action_type, reaction_code if applicable) within a 2-second window create only one queued event And events include source=lock_screen to distinguish from in-app captures
Security, Privacy, and Network Isolation
Given offline events are stored on-device When inspecting storage and runtime behavior Then events are written only to the app’s sandbox using OS-backed encryption/file protection where available And no PII beyond required identifiers (user_id, habit_id/target_id) is stored; no free-form text is captured And no network requests or background sync attempts are made while the device is offline And reading the queue requires app process access; other apps cannot read the data
Local Event Timestamping & Ordering
"As a daily habit tracker, I want my offline actions to be recorded with the correct time and order so that my streaks and activity history reflect what I actually did."
Description

When offline, assign events a trusted local timestamp and deterministic ordering. Use a monotonic clock source to reduce drift, include timezone and offset metadata, and attach an incrementing local sequence number per room to preserve the order of check-ins and reactions. Capture device-time integrity metadata for later reconciliation (e.g., last-known server time) to mitigate clock skew and enable consistent server-side ordering on sync.

Acceptance Criteria
Offline Check-In Timestamp and Metadata Capture
Given the device has no network connectivity And the user submits a check-in from the lockscreen in room R When the event is recorded locally Then the event is stored with: - wallClockTimestamp (ISO 8601 with millisecond precision) - timeZone (IANA) and utcOffsetMinutes captured at creation time - monotonicTimestamp from a monotonic clock source - roomLocalSequenceNumber incremented by 1 from the last for room R - lastKnownServerTime from the most recent successful sync (or null if none) - deviceTimeIntegrity flags (e.g., monotonicSourceAvailable=true) And roomLocalSequenceNumber is strictly increasing per room with no gaps or duplicates And monotonicTimestamp is non-decreasing relative to the prior locally recorded event And the event is durably persisted to local storage before user feedback indicates success
Per-Room Sequence Ordering on Sync
Given there are N offline events for room R with roomLocalSequenceNumber S..S+N-1 And connectivity is restored When the client sync process begins Then events for room R are transmitted and applied in ascending roomLocalSequenceNumber order And no event with a higher roomLocalSequenceNumber is applied before a lower one in the same room And sequence numbers are scoped per room so events from different rooms do not collide And the server acknowledges each applied event; the client marks them as synced in the same order And retries, batching, or out-of-order network delivery do not change the applied order within room R
Clock Changes While Offline (Skew and Backdating)
Given the device clock is adjusted forward or backward while offline And the user records events before and after the adjustment When each event is created Then monotonicTimestamp for each new event is greater than or equal to the previous event's monotonicTimestamp And wallClockTimestamp reflects the device wall time at creation and may move backward/forward And deviceTimeIntegrity.clockAdjustedSinceLastSync is set to true for events after a detected clock change And lastKnownServerTime at creation is stored on each event And on sync, the client includes monotonicTimestamp and lastKnownServerTime to enable consistent server-side ordering despite wall-clock skew
Timezone/Offset Changes During Travel
Given the user records events while offline across a timezone boundary (e.g., during a flight) When events are created before and after the timezone change Then each event stores the timeZone (IANA) and utcOffsetMinutes observed at its creation time And roomLocalSequenceNumber continues uninterrupted and increasing per room And on sync, events retain their original local-time context via stored timeZone/offset metadata
Offline Reaction Ordering and Parent Dependency
Given the user creates a check-in and then a reaction to that check-in while offline in room R When connectivity is restored and events are synced Then the check-in is applied before its associated reaction And the reaction references its parent via a stable local ID that is remapped to the server ID upon acknowledgment And if the parent check-in fails to apply, the reaction is not applied and remains pending with a failure reason recorded
Persistence Across App Restarts and Reboots
Given there are one or more queued offline events And the app is force-closed or the device reboots while still offline When the app is relaunched Then all queued events remain with original wallClockTimestamp, timeZone/offset, monotonicTimestamp, lastKnownServerTime, and roomLocalSequenceNumber intact And the next new event for room R uses roomLocalSequenceNumber = last persisted + 1 And no duplicate or skipped sequence numbers are generated per room after restart And when connectivity returns, events sync successfully without data loss
Persistent Encrypted Offline Queue
"As a privacy-conscious user, I want my offline check-ins and reactions saved securely on my device so that nothing is lost and my data remains protected until it syncs."
Description

Store queued offline events in an encrypted local database (e.g., SQLite/Room/Core Data) with crash-safe transactions and automatic recovery. Persist event type, target room, local UUID, timestamp, sequence number, minimal payload, and integrity metadata. Enforce storage quotas, eviction policies (oldest-first after threshold), and automatic cleanup after successful sync. Protect data at rest using platform key stores and respect device-level encryption settings.

Acceptance Criteria
Offline Check-in/Reaction Queuing with Required Fields
Given the device is offline and the user submits a check-in or reaction from lockscreen or in-app When the event is enqueued Then a row is persisted with fields: event_type, room_id, local_uuid (v4), device_timestamp (ms; ISO-8601 with timezone), sequence_number (monotonic per room), minimal_payload (<= 1 KB), integrity_metadata (e.g., HMAC over fields) And local_uuid is unique across all rows And sequence_number increments by 1 from the last stored for that room And the enqueue operation completes within 150 ms at P95 on target devices
Encryption at Rest via Platform Keystore
Given the offline queue database and any journal/WAL files are created Then all queue data at rest is encrypted using a key stored in the platform keystore/keychain with non-exportable attributes And no plaintext of event_type, room_id, or minimal_payload is recoverable by inspecting on-disk files When the device is in a pre-first-unlock state (file-based encryption locked) Then attempts to open the database without keystore access do not yield plaintext and fail safely without crashing
Crash-Safe Transactions and Automatic Recovery
Given an event write is in progress When the app is force-killed or the device loses power mid-write Then on next launch no partial or duplicated events exist And per-room sequence_number continuity is preserved (no gaps created by partial writes) And incomplete transactions are rolled back automatically And startup recovery completes within 500 ms P95 with a 10,000-event queue
Storage Quota and Oldest-First Eviction
Given a configured quota of 10,000 events or 20 MB total on-disk size (whichever is reached first) When adding a new event would exceed the quota Then the system evicts the oldest events first until the new event fits And it never evicts newer unsent events before older unsent events And remaining events maintain correct per-room sequence ordering And an eviction metric is recorded with the count of evicted events And the enqueue still completes within 200 ms P95
Auto-Sync on Reconnect with Ordering and Cleanup
Given queued events exist and network connectivity is restored When sync begins Then events are batched and sent in ascending sequence_number order per room (max 100 events per batch) And server acknowledgments referencing local_uuid mark those events as synced And successfully synced events are deleted from local storage within 5 seconds And unsynced events remain queued And transient failures trigger exponential backoff (2s -> 4s -> 8s up to 60s) with retry And duplicate acknowledgments for already-deleted local_uuid are ignored without error
Integrity Metadata Validation and Tamper Handling
Given each event stores integrity_metadata computed at write time over required fields When an event is read for sync and its integrity check fails Then the event is not synced and is marked corrupted And the corrupted row is removed from the active queue and a metric with the local_uuid is emitted And remaining events continue to sync without blockage And no corrupted payload is transmitted to the server
Device Encryption State Respect and Lock Screen Behavior
Given the device is locked and user credentials have not been provided (file-based encryption still locked) When the user creates an offline event from the lockscreen Then no plaintext data is written to unencrypted storage or logs And if the platform supports encrypted direct-boot storage, the event is persisted there; otherwise the event is deferred in memory And after the first user unlock, any deferred events are durably persisted to the encrypted queue within 10 seconds And audit logs show no write attempts to unencrypted paths
Auto Sync & Network-Aware Backoff
"As a busy creator, I want my offline actions to sync automatically when I’m back online so that I don’t have to remember to manage them."
Description

Automatically detect connectivity restoration and sync queued events in the background with batching, retry logic, and exponential backoff with jitter. Respect battery saver, metered networks, and OS background execution limits. Support partial batch success, resumable uploads, and timeouts. Provide hooks to trigger foreground sync on app open and expose sync completion signals to the UI without interrupting user flow.

Acceptance Criteria
Background Auto-Sync on Connectivity Restoration
Given the device has queued offline check-ins/reactions and no connectivity When connectivity is restored on Wi‑Fi or cellular Then background sync starts within 10 seconds, subject to OS background limits And events are uploaded in batches not exceeding maxBatchSize=50 or maxBatchBytes=262144 And per-user event order is preserved by original timestamp And successfully uploaded events are removed from the queue And original event timestamps are preserved server-side
Exponential Backoff with Jitter on Transient Failures
Given a batch upload fails with a retryable error (HTTP 5xx, network timeout, OS transient) When a retry is scheduled Then the delay uses exponential backoff starting at 1s, doubling each attempt, capped at 5m And each delay includes +/-20% random jitter And the backoff state persists across app restarts And the backoff resets to the initial delay after any fully successful batch
Respect Battery Saver, Data Saver, and Metered Networks
Given battery saver or system data saver is enabled, or the active network is metered When the app is in background and a sync would normally start Then background sync is deferred And only a lightweight reachability check (<1KB) may run And a defer reason (battery_saver|data_saver|metered) is recorded and exposed to observers And when the device is charging or on unmetered network, the deferred sync resumes automatically
Partial Batch Success and Resumable Uploads
Given the server returns mixed per-event results for a batch When processing the response Then events marked success are removed from the queue And events marked retryable failure remain queued with their error codes for retry And non-retryable failures are marked final and are not retried And a subsequent retry sends only the failed events with original client IDs And if an upload is interrupted mid-transfer, the next attempt resumes using uploadSessionId and byte offset without duplicating events
Sync Timeouts and Safe Cancellation
Given a batch request is in flight When no response is received within requestTimeout=15s Then the request is canceled and treated as a retryable failure And the overall sync attempt aborts after overallTimeout=60s, leaving remaining events queued And all network/background resources are released on cancellation per OS requirements
Foreground Sync Hook on App Open
Given the app transitions to foreground and queued events exist When the onAppForeground hook fires Then a foreground sync starts immediately, ignoring any current backoff delay And batching, retry, and timeout rules apply identically to background sync And if data saver/metered restrictions are active and allowForegroundOnMetered=false, the sync is deferred and emits a defer reason
UI Sync Completion Signals Without User Flow Interruption
Given a sync cycle starts, progresses, or completes When UI layers subscribe to SyncEvents Then syncStarted(batchCount, queuedCount), syncProgress(sent, total), and syncCompleted(success, failedCount, deferReason?) are emitted on the main thread And no blocking UI is shown and user interactions remain responsive (p95 input latency <100ms during sync) And the UI can render a non-intrusive status/badge without requiring user action
Conflict Resolution & Idempotency
"As a user with spotty internet, I want the app to avoid duplicates and handle edge cases cleanly so that my history stays accurate even if I tapped multiple times offline."
Description

Assign each event a globally unique idempotency key and ensure the server deduplicates on receipt. Define conflict policies for late or duplicate check-ins and reactions (e.g., one check-in per day per room, latest valid timestamp wins, reaction aggregation rules). Handle cases where rooms are archived or membership changes while offline and return structured reason codes for any rejected events to inform the client for cleanup or user messaging.

Acceptance Criteria
Idempotency Key Deduplication on Server
Given an event with idempotency_key K and payload P is received for the first time When the same idempotency_key K is received again with any payload P' Then the server applies the event exactly once and does not create duplicate state And the first receipt returns 200 OK with outcome=accepted, canonical_event_id, processed_at, idempotency_key=K And subsequent receipts return 200 OK with outcome=duplicate, canonical_event_id identical to the first, prior processed_at, idempotency_key=K
Single Check-In Per Day Per Room (Latest Valid Timestamp Wins)
Given a user has multiple queued check-in events for the same room and calendar day in the room’s timezone with timestamps T1..Tn When the events are synced in any order Then the server persists exactly one check-in for that user-room-day with timestamp=max_valid(T1..Tn) And any earlier accepted check-in for that day is updated/replaced rather than duplicated And responses for superseded events include outcome=replaced and reason_code=CHECKIN_SUPERSEDED, referencing the canonical_event_id
Late Offline Check-Ins within Allowed Window
Given a server-configured late_submission_window_hours=W for check-ins And an offline check-in with timestamp T for day D is received at server time R When R - end_of_day(D, room_timezone) <= W hours Then the event is accepted subject to single-per-day rules And when R - end_of_day(D, room_timezone) > W hours Then the event is rejected with 409 Conflict, outcome=rejected, reason_code=LATE_WINDOW_EXCEEDED, and response includes server_now and allowed_window_hours=W
Reaction Idempotency and Toggle Aggregation
Given reactions are uniquely identified by (reacting_user_id, target_id, reaction_type) And the client queues reaction add/remove events with idempotency keys When the events are synced Then duplicates by idempotency_key have outcome=duplicate and do not change aggregate counts And if the last chronological action for the key tuple is add, the user’s reaction exists and aggregate count reflects +1 relative to baseline And if the last chronological action is remove, the user’s reaction does not exist and aggregate count reflects removal And each event response includes outcome in {applied, duplicate, no_op} and, when no_op, a reason_code (e.g., ALREADY_PRESENT or ALREADY_ABSENT)
Room Archived or Membership Changed While Offline
Given queued check-ins or reactions target a room or message where access has changed while offline When the server detects the room is archived or deleted, or the user lacks membership/permission at sync time Then the server rejects those events with outcome=rejected and an HTTP status of 403 (NO_ACCESS or NOT_MEMBER), 409 (ROOM_ARCHIVED), or 410 (ROOM_DELETED) as applicable And the response includes reason_code in {ROOM_ARCHIVED, ROOM_DELETED, NOT_MEMBER, NO_ACCESS}, idempotency_key, and no state changes are applied
Per-Event Structured Outcomes in Batch Sync
Given the client submits a batch of N queued events in one request When the server processes the batch Then the response contains N per-event results in the same order as submitted And each result includes idempotency_key, outcome in {accepted, duplicate, replaced, rejected, applied, no_op}, reason_code (nullable), canonical_event_id (nullable), processed_at, server_now And the sum of results where outcome in {accepted, replaced, applied} equals the number of state changes actually performed
Streak Integrity Validation Rules
"As a habit-focused user, I want my legitimate offline check-ins to count toward my streak while preventing exploits so that streaks remain motivating and fair."
Description

Validate queued events against server-side streak rules on sync to preserve fairness while preventing abuse. Enforce grace windows, reject future-dated events, detect excessive clock drift, and adjust for timezone changes and DST. Maintain an audit trail linking local sequence/timestamps to server-accepted times, and compute streak continuity updates atomically to prevent streak decay from legitimate offline usage.

Acceptance Criteria
Grace Window Enforcement for Offline Check-ins
Given a habit with a daily check-in window of 00:00–23:59 local time and a grace window of G=15 minutes And a queued offline check-in with device_local_timestamp and device_utc_offset captured at event time When the server validates the event on sync Then the event is accepted if its resolved local time ∈ [day_start, day_end + G) And the event is rejected with reason "outside_grace_window" if it falls outside that interval And only one accepted check-in per calendar day is counted toward the streak, prioritizing the earliest accepted event for that day
Future-Dated Event Rejection
Given server_current_time = T_sync at validation And a configurable future skew threshold F=120 seconds When an event's device_local_timestamp + device_utc_offset resolves to a wall-clock time > T_sync + F Then the event is rejected with reason "future_dated" And it does not update streaks or reactions And an audit record is created with the rejected reason and the computed delta_seconds
Excessive Clock Drift Detection and Handling
Given a configurable clock drift threshold D=10 minutes And a batch of N queued events with captured device_sent_at and server_received_at When |device_sent_at - server_received_at| > D for any event Then that event is flagged "excessive_clock_drift" And if the resolved local time would otherwise be accepted, the server accepts it but marks the audit record with drift=true and drift_seconds And if the drift causes the event to land outside any valid window, the event is rejected with reason "excessive_clock_drift"
Timezone Change and DST Adjustment on Sync
Given queued events each include device_local_timestamp and device_utc_offset_at_event And the user changed timezone while offline and/or DST transitioned When validating events Then the server resolves the event's calendar day using device_utc_offset_at_event (not the current offset) And on DST fall-back, two events at the same local clock time but different offsets are treated as distinct and mapped to their correct UTC instants And on DST spring-forward, events whose local time does not exist are mapped using the correct UTC instant computed from the provided offset and are validated against the correct calendar day And streak counting uses the resolved calendar day per event; no duplicate or missing day counts are introduced by DST or timezone changes
Audit Trail Linking Local to Server
Given each queued event includes client_event_id (UUID), local_sequence_number, device_local_timestamp, and device_utc_offset When events are processed Then the server persists an immutable audit record linking client_event_id and local_sequence_number to server_received_at, server_resolved_local_time, decision (accepted/rejected), reason_code, and applied_day And an authenticated GET /events/audit?since=... returns these records within 200 ms per 100 items with stable ordering by server_received_at And rejected events appear with decision=rejected and a non-empty reason_code; accepted events include the streak_id updated (if any)
Atomic Streak Continuity Update Across Queued Events
Given a batch of accepted check-ins spanning multiple days When computing streak continuity on sync Then updates are applied atomically within a single transaction; either all streak day increments and decay-prevention flags are committed, or none are And if a failure occurs, the transaction is rolled back and retrying the same batch yields identical final streak state (idempotent) And streak length equals the count of unique accepted days after processing; no partial-day increments are visible mid-transaction And concurrent syncs for the same user serialize to prevent race conditions (e.g., via per-user mutex/row lock)
Out-of-Order, Duplicate, and Reaction Validation
Given queued events may arrive out-of-order and may contain duplicates And each event has client_event_id and local_sequence_number When validating Then events are de-duplicated by client_event_id; duplicates are ignored with reason "duplicate" And check-ins are ordered by resolved UTC time for validation, with local_sequence_number used as a stable tiebreaker And reactions are accepted only if they reference an existing accepted check-in within the same room/day; otherwise rejected with reason "orphan_reaction" And out-of-order arrival does not change the accepted results compared to in-order arrival
User Feedback & Manual Retry Controls
"As a user who travels often, I want clear indicators when actions are queued and easy ways to retry or discard them so that I feel confident nothing is lost."
Description

Surface lightweight UI indicators for queued state and sync progress (e.g., badges, banners), with accessible status messages. Provide a simple offline queue view that shows pending items, per-item status, and options to retry now or discard safely. Notify users on successful sync or final failure with actionable guidance, while redacting sensitive content on the lock screen for privacy.

Acceptance Criteria
Lockscreen Offline Queue Indicator with Redaction
Given the device is offline and the user performs a lockscreen check-in or reaction, When the action is submitted, Then a lockscreen indicator shows "Queued" within 500ms and the queued count increments. Given queued items are present on the lockscreen, When a screen reader is enabled, Then an accessible announcement states "Item queued; will sync when online" and the indicator exposes an accessible name and value. Given privacy requirements, When the lockscreen indicator is shown, Then no habit title, room name, note text, or reaction content is displayed—only generic labels are shown.
In-App Sync Progress Banner and Badge
Given the app is open with N queued items, When network connectivity is restored, Then a banner appears within 1 second stating "Syncing N items" and a navigation badge shows the same count. Given syncing is in progress, When an item completes, Then the banner and badge decrement in real time and disappear within 1 second when all items are synced and no failures remain. Given one or more items fail, When syncing completes, Then the banner changes to "1 failed" (pluralized as needed) with a "View" action that opens the Offline Queue filtered to Failed items.
Offline Queue View with Per-Item Controls
Given queued items exist, When the user opens the Offline Queue view, Then items are listed oldest-first with type (check-in/reaction), timestamp, target room label, and status (Queued, Syncing, Failed, Synced). Given an item row, When the user taps Retry Now, Then the item state changes to Syncing immediately; When the user taps Discard, Then a confirmation dialog appears and, on confirm, the item is removed and will not sync. Given the item is a check-in within an active streak window, When Discard is confirmed, Then the dialog warns about potential streak impact before removal. Given the queue has no items, When the view is opened, Then an empty state is shown with guidance and no errors.
Manual Retry Now and Retry All Behavior
Given at least one Failed or Queued item and the device is online, When the user taps Retry Now on an item, Then a sync request starts within 300ms and the state changes to Syncing with visible feedback. Given multiple Failed items and the device is online, When the user taps Retry All, Then all eligible items attempt sync immediately and update their statuses without waiting for background retries. Given the device is offline, When the user attempts Retry Now or Retry All, Then the controls are disabled and an accessible tooltip/toast explains "No connection—will retry when online."
Success and Failure Notifications with Guidance
Given the app is backgrounded with queued items, When items successfully sync, Then a single aggregated system notification states "X items synced" and does not reveal habit names or note content on the lock screen. Given an item reaches final failure, When the app is foreground or background, Then a notification or in-app toast states "1 item failed to sync" with actions "Retry" and "Open Queue" plus a brief cause summary (e.g., "No connection", "Authentication needed"). Given resolution requires user action (e.g., sign-in), When the user taps the notification action, Then the app deep-links to the required screen and returns to the Offline Queue afterward.
Accessibility and Announcements for Queue and Sync
Given a screen reader is enabled, When queued counts change or sync starts/completes, Then polite live announcements describe the update without disrupting current focus. Given the Offline Queue view, When navigating via accessibility services, Then all interactive elements have descriptive accessible names, roles, and states; focus order matches visual order; text scales with system settings; and colors meet WCAG 2.1 AA contrast. Given status must not rely on color alone, When displaying Queued, Syncing, Failed states, Then the UI includes text and/or iconography to convey status in addition to color.

Big Tap Mode

Adaptive, larger tap targets on the lockscreen and watch during motion or low light, reducing mis-taps while walking or between meetings. Keeps one-tap speed accessible anywhere.

Requirements

Auto-Activation via Motion/Light Detection
"As a remote worker walking between meetings, I want Big Tap Mode to auto-activate when I’m in motion so that I can check in with one tap without mis-taps."
Description

Automatically enables Big Tap Mode when motion or low-visibility conditions are detected, minimizing mis-taps while users are walking, commuting, or moving between meetings. Uses platform activity recognition (e.g., walking, running, in vehicle) and context signals (e.g., wrist-raise on watch, system-provided low-visibility cues) to trigger larger tap targets for core actions like Check In and React. Includes configurable thresholds and hysteresis to avoid rapid toggling, per-user opt-in, and explicit controls to override (manual on/off) within StreakShare. Integrates with permissions and respects battery constraints by batching sensor reads and leveraging OS classifiers. Big Tap state propagates across phone, watch, and lockscreen widgets for consistent experience.

Acceptance Criteria
Auto-Activate on Motion with Hysteresis
Given the user has opted in and granted required motion/activity permissions And the OS reports activity as walking, running, or in vehicle continuously for >= 5 seconds When StreakShare is running (foreground, background, or lockscreen widget visible) Then Big Tap Mode turns ON within 1 second And a Big Tap indicator is visible on applicable surfaces And the mode does not toggle more than once in any rolling 10-second window Given the OS reports the user is still for >= 15 seconds and no low-visibility trigger is active Then Big Tap Mode turns OFF within 2 seconds
Auto-Activate on Low Visibility
Given the user has opted in And the platform exposes a low-visibility cue (e.g., ambient light low, system-provided low-light/dim state) continuously for >= 3 seconds When StreakShare is eligible to present core actions Then Big Tap Mode turns ON within 1 second And Check In and React tap targets increase in size per Big Tap specifications on the current surface Given visibility returns to normal for >= 20 seconds and motion triggers are not active Then Big Tap Mode turns OFF within 2 seconds
Manual Override Takes Precedence Over Auto
Given the user sets Big Tap to Manual On in StreakShare When motion or low-visibility auto-activation conditions occur or cease Then Big Tap remains ON until the user changes the setting And this state persists across app restarts and device reconnections Given the user sets Big Tap to Manual Off Then Big Tap remains OFF regardless of auto-activation triggers Given the user switches the mode back to Auto Then automatic activation/deactivation resumes immediately on the next qualifying detection
Opt-In and Permissions Gatekeeping
Given the user has not opted in to Auto-Activation When a qualifying motion or low-visibility condition is first detected Then the app presents a single opt-in explainer with a clear Allow/Not Now choice And, upon Allow, requests required OS permissions using native dialogs If the user denies or selects Not Now, then auto-activation is not enabled And the user is not re-prompted automatically again, but can enable from Settings If the user allows, then auto-activation proceeds on the next qualifying detection without requiring app relaunch
State Propagation Across Phone, Watch, and Widgets
Given the phone and watch are connected When Big Tap state changes on either device (auto or manual) Then the other device and all lockscreen/home widgets reflect the new state within 3 seconds Given devices are disconnected When they reconnect Then all surfaces converge to the latest state within 10 seconds using last-writer-wins And visual indicators/icons match the active state consistently across surfaces
Battery and Resource Constraints Compliance
Given StreakShare is in background or the screen is off Then the app uses OS activity recognition/classifiers and does not perform continuous high-rate accelerometer polling (> 1 Hz) And sensor/event handling is batched with a minimum processing interval of 15 seconds when permissible by the OS And no wake locks/wakeups are held longer than 1 second per transition During a 30-minute mobility test with auto-activation enabled, battery drain attributable to StreakShare increases by no more than 1% absolute over baseline without the feature
Graceful Degradation When Signals/Permissions Missing
Given required permissions are missing or OS motion/visibility classifiers are unavailable When qualifying conditions occur Then Big Tap does not auto-activate And StreakShare surfaces a non-intrusive notice in Settings explaining how to enable Auto-Activation And manual Big Tap controls continue to function without error And the app logs the condition without crashing or freezing
Enlarged Tap Targets & Hit Slop
"As a creator on the go, I want larger touch targets for primary actions so that I can reliably check in with a single tap."
Description

Increases minimum tap target size and hit slop for primary actions in StreakShare’s micro-commitment rooms and widgets during Big Tap Mode. Establishes size rules (e.g., ≥72dp touch areas, expanded hit areas, spacing) and simplifies layouts to ensure single-handed, glanceable use in motion. De-prioritizes or hides non-essential micro-actions to reduce clutter; debounces multi-taps and prevents accidental double submissions. Applies consistently across app views, lockscreen widgets, and watch UIs, preserving one-tap speed while reducing error rate. Includes visual affordances indicating Big Tap Mode is active.

Acceptance Criteria
Platform-Wide Minimum Target Sizes & Spacing in Big Tap Mode
Rule: Applies to micro-commitment room, in-app widgets, lockscreen widgets, and watch UIs when Big Tap Mode is active. Rule: Primary action tap target minimum size — Android: >=72dp x 72dp; iOS: >=44pt x 44pt; Wearables (Wear OS/watchOS): >=48dp or >=7mm, whichever is greater. Rule: Hit slop expansion (all sides) — Android: +12dp; iOS: +8pt; Wearables: +6dp. Rule: Inter-target spacing (edge-to-edge) — Android: >=12dp; iOS: >=8pt; Wearables: >=6dp. Rule: No tappable target’s hit area may overlap another’s hit area. Rule: Layout remains within safe areas with no horizontal scroll or clipped controls in portrait.
Lockscreen Walking Check-In Accuracy
Given Big Tap Mode is active on the phone lockscreen widget When a user performs 200 single-tap attempts on the primary Check-In while walking (simulated motion enabled) Then at least 198/200 taps activate the primary action And 0/200 taps activate any secondary action And visual confirmation of success appears within 200ms of the tap
Watch Low-Light Check-In Reliability
Given Big Tap Mode is active on the watch check-in screen due to low ambient light When the user performs 100 single taps on the primary action Then 100% of taps with a touch inside the target + hit slop activate the primary action And 0 taps activate any other action And a confirmation haptic or visual signal appears within 200ms And the primary target meets the wearable minimum size and hit slop rules
Debounce & Double-Submit Prevention Across Surfaces
Given Big Tap Mode is active on any supported surface When the user taps the primary action 3 times within 600ms Then exactly one submission is sent to the backend And the UI disables the primary target for 600ms and shows a submitted state And no duplicate entries appear in the activity feed And additional taps during the debounce window produce no additional haptics or submissions
Non-Essential Actions Deprioritized in Big Tap Mode
Rule: Only primary actions defined for the view (e.g., Check-In, Undo) are visible on the first interaction layer; all other actions are hidden or placed behind a More menu. Rule: The first interaction layer contains no more than 2 actionable tap targets. Rule: Secondary actions, if exposed, require two steps to execute (More -> Action). Rule: Reaction trays and text inputs are collapsed by default in Big Tap Mode. Rule: All hidden actions remain accessible via the More menu to preserve functionality.
Visual Indicator of Big Tap Mode Active
Given Big Tap Mode is toggled on When any supported surface is displayed Then a persistent on-screen badge with the text "Big Tap" is visible in the header or corner And primary action targets render with an enhanced affordance (e.g., outline >=2dp or elevated shadow >=2dp) And toggling Big Tap Mode on/off triggers a single haptic and an accessibility announcement of the mode state
One-Tap Speed Preserved Under Big Tap Mode
Given Big Tap Mode is active When the user taps the primary Check-In Then time from tap-up to local visual confirmation is <=200ms (median) and <=350ms (p95) measured over 100 actions per device class And Big Tap Mode does not increase initial render time of affected surfaces by more than 5% compared to baseline (same device, same build) And no dropped frames occur during the tap feedback animation on reference devices (>=60 FPS sustained during feedback)
Lockscreen Big Tap Widget
"As a busy user, I want a large lockscreen check-in button so that I can maintain my streak without unlocking or navigating the app."
Description

Provides an interactive lockscreen widget optimized for Big Tap Mode that exposes a single large Check In action and glanceable status (current room/habit, streak count, last check-in time). Enlarged tap area and clear confirmation state minimize mis-taps in low-attention moments. Supports deep links to the active room when additional context is required. Works offline by queuing check-ins and syncing on reconnect. Honors system privacy (hides sensitive text on secure lock) and respects DND/Focus modes. Uses platform interactive widget frameworks to ensure performance and battery efficiency.

Acceptance Criteria
One-Tap Check-In Under Low Light or Motion
Given the Big Tap Mode lockscreen widget is visible When the user taps the large Check In button Then a check-in is recorded within 1 second and a success state is displayed within 200 ms And the primary tap target's hit area is at least 72pt x 72pt (iOS) or 72dp x 72dp (Android) And only one primary action is present with no secondary action buttons visible And taps with touch-up inside are accepted with up to 10pt movement tolerance; touch-up outside cancels the action
Glanceable Status: Habit, Streak, Last Check-In
Given the Big Tap Mode widget is displayed with an active habit When the user glances at the widget Then it shows the current habit/room name (max 20 characters + ellipsis), streak count, and last check-in as relative time (e.g., "2h ago") And the streak count numeral height is >= 16pt and text contrast meets WCAG AA (>= 4.5:1) And the glanceable status updates within 2 seconds after a successful widget check-in
Privacy: Secure Lock and Focus Mode Behavior
Given the device is on a secure lock screen with "Hide Sensitive" enabled or a Focus/DND mode is active When the widget renders Then the habit/room name and last check-in text are replaced with generic placeholders; only streak count and a generic icon are shown And tapping any area that would reveal context requires unlock before app content is shown And no sounds or continuous animations are produced while Focus/DND is active
Offline Queueing and Sync of Check-Ins
Given the device has no network connectivity When the user taps the large Check In button Then the check-in is stored locally with a timestamp and the button shows "Queued" within 200 ms And upon connectivity restoration, the queued check-in syncs within 10 seconds and the streak updates accordingly And repeated taps while offline result in at most one check-in per habit cadence interval (idempotent) And failed syncs retry with exponential backoff for up to 24 hours and display a "Retrying" state
Deep Link to Active Room from Widget
Given there is an active room for the current habit When the user taps the status area (not the primary Check In button) Then the app opens to streakshare://room/{room_id} within 1 second after unlock And if no active room exists, the app opens to the Habits screen And if the device is locked, the deep link is deferred and completes after authentication
Confirmation State and Mis-Tap Prevention
Given the user has tapped the Check In button When the check-in is accepted Then the widget shows a checkmark + "Checked In" state for at least 2 seconds and disables the button during that interval And a light haptic feedback is emitted if supported And the button ignores repeat taps for 2 seconds after success And drags leaving the button bounds before touch-up cancel the action
Performance and Battery Efficiency Compliance
Given the widget is installed and used under normal conditions When measuring interaction and update behavior Then the widget uses only platform interactive widget APIs without background polling And average widget extension CPU time per tap is < 50 ms and memory < 20 MB on reference devices And idle timeline reloads occur no more than once per hour; interactive updates do not exceed platform rate limits And there are no crashes or ANRs attributable to the widget in a 7-day soak test with 500 sessions
Watch Big Tap UI & Offline Check-in
"As a watch user during a commute, I want a big, offline-capable check-in on my wrist so that I can record progress even without my phone."
Description

Delivers a watch-optimized Big Tap interface with a single primary button for Check In and optional secondary actions (e.g., React) using large, high-contrast targets. Supports wrist-raise quick access, left/right hand orientation, and complications for one-tap entry. Caches check-ins offline when the phone is unavailable and reconciles on reconnection with conflict handling. Provides short haptic confirmation and a brief undo option. Minimizes scrolling and navigational depth to accommodate in-motion usage while preserving streak integrity.

Acceptance Criteria
Wrist-Raise Quick Access to Big Tap
Given Quick Access is enabled and StreakShare was the last-used app within 120 seconds When the user raises their wrist Then the Big Tap screen is shown as the first view within 1.0 second and the primary Check In button is immediately tappable Given Quick Access is disabled or StreakShare was not the last-used app When the user taps a StreakShare complication Then the Big Tap screen opens within 1.0 second and is ready for input
Big Tap Primary Button Size, Contrast, and In-Motion Adaptation
Given the Big Tap screen is displayed Then the primary Check In button has a minimum touch target of 44x44 pt and occupies at least 35% of vertical screen space on 40–49mm watches And secondary actions each have minimum 44x44 pt targets with at least 8 pt separation And text/icons on all actionable elements meet a contrast ratio >= 7:1 against background in light and dark themes And no scrolling is required to reveal the primary Check In button Given the watch detects in-motion state or low ambient light When the Big Tap screen is displayed Then the primary Check In target increases to at least 56x56 pt without clipping other actionable elements Given the user is on the Big Tap screen Then secondary actions are accessible within one tap And no screen in this flow exceeds a navigation depth of 1 from the Big Tap screen
Offline Check-In Capture Without Phone
Given the watch has no network connectivity and the paired phone is unavailable When the user taps the Check In button Then the check-in is recorded locally with habitId, UTC timestamp, and a UUID And a short haptic confirmation plays within 150 ms And the UI reflects a "Checked in" state And up to 100 pending check-ins are retained across app relaunches and watch reboots until synced
Offline Reconciliation and Conflict Handling
Given one or more cached check-ins exist locally When the watch regains connectivity or the phone reconnects Then cached check-ins are uploaded in UTC timestamp order within 10 seconds And if a cached check-in matches an existing server check-in for the same habit and streak period Then no duplicate is created (idempotency via habitId+period+UUID) And if multiple cached check-ins exist for the same habit and streak period Then only the earliest is kept and later duplicates are discarded locally without server creation And upon successful sync, local cache entries are cleared and the streak count matches the server within two refresh cycles
Haptic Confirmation and Undo Window
Given a check-in is submitted online or cached offline Then a short haptic confirmation plays within 150 ms And an Undo control is shown for 5 seconds When the user taps Undo within 5 seconds Then the check-in is reversed locally immediately and marked for reversal on the server at next sync And streak metrics revert to their prior state locally When 5 seconds elapse without Undo Then the Undo control disappears and the check-in remains committed Given rapid repeated taps on the primary button within the 5-second window Then no more than one check-in is recorded for the same habit and streak period
Handedness Orientation Support
Given the user selects Left-Hand Mode in watch settings for StreakShare When the Big Tap screen loads Then the primary Check In button is left-aligned and secondary actions are right-aligned And all tappable targets maintain >= 8 pt safe insets from edges Given the user selects Right-Hand Mode When the Big Tap screen loads Then the layout is mirrored accordingly Given an orientation is selected When the app restarts or the watch reboots Then the selected orientation persists
One-Tap Entry via Complications
Given StreakShare complications (Utility, Modular, Corner) are installed When the user taps any StreakShare complication Then the app opens directly to the Big Tap screen within 1.0 second Given complication data is permitted to refresh by the system When the streak state changes or 30 minutes elapse (whichever is sooner) Then the complication updates its displayed state And complication data is never older than 2 hours Given today’s habit is already checked in Then the complication shows a checked indicator; otherwise it shows a ready indicator
Haptic/Audio Feedback with Undo
"As someone moving quickly, I want clear haptic confirmation and an undo option so that I know my tap registered and can fix mistakes fast."
Description

Confirms taps immediately with distinct haptic feedback and subtle audio cues to reassure users that a check-in or reaction was recorded, even when they cannot visually verify. Patterns differentiate success, failure, and undo states, and honor system mute/DND settings. Displays a lightweight, time-bound undo affordance (e.g., 3–5 seconds) to correct accidental taps without slowing one-tap flow. Consistent behavior across phone, lockscreen widget, and watch ensures predictability in motion.

Acceptance Criteria
Lock Screen Check-In Success Feedback in Big Tap Mode
Given Big Tap Mode is active on the lock screen widget and the user taps the check-in target When the tap is recognized Then a Success haptic (HapticID.Success) starts within 100 ms of recognition And a Success audio cue plays within 150 ms unless system mute or DND is enabled And the check-in is committed locally and queued for sync if offline And an Undo control appears within 150 ms and remains visible for 4 s (configurable 3–5 s) And the presence of the Undo control does not block subsequent taps on other controls
Watch Reaction Tap Confirmation While Walking
Given the user is wearing a watch and motion is detected (walking) and the user taps a reaction When the tap is recognized Then a Success haptic (HapticID.Success) starts within 75 ms of recognition on watchOS And a Success audio cue plays within 150 ms unless system mute or DND is enabled on the watch And the reaction is committed locally and queued for sync if offline And an Undo control appears within 150 ms and remains visible for 4 s (configurable 3–5 s)
Failure Feedback and Retry on Record Error
Given the user attempts a check-in or reaction and local commit or server acknowledgment fails When the failure is detected Then a Failure haptic (HapticID.Failure) plays within 100 ms of failure detection And a Failure audio cue plays within 150 ms unless system mute or DND is enabled And no streak or reaction state change is persisted locally And no Undo control is shown for failed actions And a non-blocking retry option is available within the current UI context
Undo Within Time Window Reverts State
Given a check-in or reaction has been recorded and the Undo control is visible When the user activates Undo within the visible window Then the local state reverts within 150 ms and the action is removed from the timeline/streak count And an Undo haptic (HapticID.Undo) plays within 100 ms and an Undo audio cue plays within 150 ms unless system mute or DND is enabled And the server is updated to revert within 1 s when online, or the revert is queued for sync when offline And after the Undo window expires, the Undo control disappears and the action remains final
System Mute and Do Not Disturb Compliance
Given system mute or Do Not Disturb is enabled on the device When the user performs a check-in, reaction, or undo Then no audio cues play And haptics still play unless system haptics are disabled And when both audio and haptics are disabled at the system level, the app emits no audio or haptic feedback
Cross-Device Consistency of Patterns and Latency
Given the same action (success, failure, undo) is performed on phone app, lock screen widget, and watch app When feedback is emitted Then the same semantic mapping is used: Success -> HapticID.Success/AudioID.Success, Failure -> HapticID.Failure/AudioID.Failure, Undo -> HapticID.Undo/AudioID.Undo And first-feedback latency targets are met on each surface (<=100 ms haptic, <=150 ms audio for phone and lock screen; <=75 ms haptic, <=150 ms audio for watch) And perceived pattern distinctness is maintained across surfaces (no reuse of the same haptic/audio ID for different outcomes)
Resource & Privacy Safeguards
"As a privacy-conscious user, I want motion detection to be efficient and on-device so that my battery and data stay protected."
Description

Implements guardrails that limit sensor usage and protect user privacy while enabling Big Tap Mode. Leverages OS-level activity recognition APIs rather than continuous raw sensor streaming, batches detections, and disables sampling when the app is background-restricted or battery saver is active. All detection runs on-device; no raw motion data is stored or transmitted. Features are permission-gated with clear in-app education and a one-tap revoke path. Includes a remote kill switch and a safe fallback (manual toggle) if anomalies are detected.

Acceptance Criteria
Activity Recognition API Only (No Raw Sensor Streaming)
Given Big Tap Mode is enabled, When motion context is requested, Then only OS-level activity recognition APIs are used and no continuous raw accelerometer/gyroscope streams are subscribed. Given instrumentation is enabled, When the app runs for 10 minutes during motion, Then the count of active raw motion sensor listeners equals 0 while activity recognition callbacks are > 0. Given the feature is active on phone and watch, When runtime sensors are inspected, Then neither client registers raw accelerometer/gyroscope listeners for Big Tap Mode.
Batched Detection Windows Under Motion
Given the device is in motion, When motion state updates are produced for Big Tap Mode, Then detections are batched at a minimum interval of 15 seconds (configurable) and not more than 4 background wakeups occur per 15 minutes. Given a 30-minute walking session with the app in background, When profiling resource usage, Then average CPU attributable to Big Tap Mode is <= 1% and no partial wake lock exceeds 1 second per batch. Given the app returns to foreground, When a new batch is available, Then UI adjustments derived from motion occur no more than once every 5 seconds to prevent flicker.
Respect Battery Saver and Background Restrictions
Given Battery Saver is enabled or the app is background-restricted by the OS, When Big Tap Mode is active, Then motion detection sampling is suspended within 5 seconds and no activity recognition callbacks are registered during restriction. Given restrictions are lifted, When the user brings the app to foreground or disables Battery Saver, Then motion detection resumes within 5 seconds. Given restrictions persist, When the user attempts to enable Big Tap Mode, Then the UI displays a non-blocking notice that reduced functionality applies and the feature remains off.
On-Device Processing and Zero Raw Motion Data Persistence/Transmission
Given Big Tap Mode is active, When motion events are processed, Then all inference occurs on-device and no raw motion data is written to disk or retained beyond volatile memory for the current batch. Given network traffic is captured over 30 minutes of use, When requests are inspected, Then no payloads or headers contain raw accelerometer/gyroscope samples. Given the app crashes or is force-closed, When relaunched, Then no raw motion data from the prior session is present in local storage.
Permission Gating with Education and One-Tap Revoke
Given the user enables Big Tap Mode for the first time, When toggled on, Then an education screen explains purpose, data use, and privacy in <= 120 words with a link to policy, and the OS permission prompt appears only after explicit in-app consent. Given the user denies OS motion permission, When returning to the feature, Then Big Tap Mode remains off and a single non-nagging retry affordance is shown per session. Given the user taps "Revoke Motion Access", When the action is confirmed, Then OS permission is revoked and Big Tap Mode disables within 1 second across phone and watch. Given permission is revoked, When background services start, Then no motion listeners are registered.
Remote Kill Switch and Safe Fallback to Manual Toggle
Given a remote kill flag is activated, When the device receives configuration, Then Big Tap Mode auto-disables within 5 minutes and the UI falls back to manual toggle with standard tap targets. Given the device is offline, When a cached kill flag exists or a local anomaly threshold is exceeded (e.g., crash rate > 5% in 60 minutes or drain > 10%/hour attributable to the feature), Then Big Tap Mode disables on next app launch and manual toggle is presented. Given the kill switch is active, When the user attempts to re-enable Big Tap Mode, Then an informative notice is shown and re-enable is blocked until the flag is cleared. Given the kill flag is cleared remotely, When new configuration is received, Then availability is restored without reinstall and the user must re-consent to motion permissions.
Mis-tap Telemetry & Experimentation
"As a product owner, I want to measure mis-taps and run experiments so that we can prove Big Tap Mode improves on-the-go adherence."
Description

Instruments anonymized metrics to quantify Big Tap Mode impact, including tap error rate, undo rate, time-to-check-in from surface, and completion rates during motion windows. Adds experiment flags to compare default UX versus Big Tap variants (sizes, hit slop, feedback patterns) and supports rollout by cohort. Dashboards surface trends by device class and surface (app, lockscreen, watch) to guide iteration while respecting privacy and opt-out. Success criteria target measurable reduction in mis-taps and increased on-the-go check-ins without added friction.

Acceptance Criteria
Unified Tap Telemetry Across Surfaces
Given telemetry is enabled and the user interacts with any tap target on app, lockscreen, or watch When a tap attempt occurs Then the client logs a TapAttempt event with fields: surface, deviceClass, bigTapVariantId, hitResult (hit|miss), hitSlopApplied (true|false), timestampMs (UTC), sessionId, motionState (moving|stationary|unknown), ambientLightLevel (bucketed), and anonymizedUserId Given a TapAttempt results in a successful check-in When the check-in is confirmed Then a CheckInComplete event is logged with correlationId linking to the TapAttempt and includes timeToCheckInMs Given network connectivity is unavailable When events are buffered locally Then events are persisted encrypted-at-rest, capped at 1 MB or 1000 events per batch, and uploaded within 15 minutes of connectivity restoration
Undo Events Linked to Preceding Check-ins
Given a user completes a check-in via any surface When the user taps Undo within the allowable undo window Then an Undo event is logged with correlationId of the originating CheckInComplete, surface, deviceClass, bigTapVariantId, timestampMs, and reason (user_action) Given Undo and CheckInComplete events exist for a period When metrics are computed hourly Then undoRate = Undo/CheckInComplete is calculated per surface, deviceClass, bigTapVariantId, and motionState and exposed to dashboards
Time-to-Check-in Measurement From Surface Exposure
Given a check-in surface becomes visible to the user When the first frame is rendered Then a SurfaceExposed event is logged with surface, deviceClass, bigTapVariantId, timestampMs, and motionState Given a SurfaceExposed event precedes a CheckInComplete When the CheckInComplete is logged Then timeToCheckInMs = CheckInComplete.timestampMs - SurfaceExposed.timestampMs is computed on the client and included in the event payload Given timeToCheckInMs values are ingested When dashboard tiles are generated Then P50, P90, and P95 time-to-check-in are displayed per surface, deviceClass, bigTapVariantId, and motionState with daily freshness (<4h latency) and ±50 ms instrumentation accuracy verified by synthetic tests
Motion Windows Detection and Completion Rates
Given device motion state is available from OS APIs When motion exceeds configured thresholds (e.g., walking cadence or speed > 0.5 m/s for ≥5s) Then a MotionWindowStart event is logged with motionType (walking|running|unknown) and a corresponding MotionWindowEnd when motion subsides Given MotionWindow events and CheckInComplete events exist When completion metrics are computed Then completionRateMoving and completionRateStationary are calculated and exposed per surface, deviceClass, and bigTapVariantId, with 95% confidence intervals and minimum cell size ≥ 100 users enforced
Server-Driven Big Tap Experiments by Cohort
Given Big Tap experiment definitions exist server-side When a user becomes eligible Then they are assigned deterministically to a variant using anonymizedUserId hashing, and an ExperimentExposure event is logged with experimentId, variantId, cohort attributes, and surface Given cohort filters (e.g., deviceClass, new_vs_returning, high_motion_user) When a rollout is configured Then only users matching the cohort receive variant assignment, and a kill-switch can disable the experiment within 5 minutes globally Given guardrail metrics are configured (crash rate, ANR rate, battery impact, latency) When any guardrail breaches thresholds Then the experiment auto-pauses and emits an ExperimentPaused event with reason and affected cohorts
Privacy-Safe Dashboards by Surface and Device Class
Given telemetry data is aggregated daily and hourly When dashboards render metrics Then tap error rate (miss/(hit+miss)), undo rate, time-to-check-in percentiles, and completion rates are displayed segmented by surface, deviceClass, bigTapVariantId, and motionState with filterable date ranges Given privacy constraints are enabled When any cell has fewer than 50 unique users in the interval Then the cell is suppressed or noise-added to meet k-anonymity ≥ 50, and no raw event payloads or PII are exposed in the dashboard Given dashboard data pipelines run When data freshness is checked Then metrics are updated within <4 hours of event ingestion and show last-refresh timestamps
User Privacy, Opt-Out, and Data Retention
Given the user has not opted out of analytics When telemetry and experimentation run Then only anonymized identifiers are used; no precise location, raw tap coordinates, or content data are collected; and retention policies cap raw events at 30 days with aggregated metrics retained Given the user opts out of analytics or disables Big Tap experiments When the setting is saved Then no further telemetry or ExperimentExposure events are sent, local buffers are purged within 24 hours, and the user is excluded from experiments within 5 minutes Given a user requests account deletion When the request is processed Then all associated raw telemetry is deleted within 7 days and the user is removed from cohorts in active experiments

Pop-In Unlock

Launch StreakShare from a push, widget, or watch and authenticate with FaceID/TouchID via passkey in a blink. You’re deep-linked straight to the right room and habit, primed for a one-tap check-in. Cuts re-entry to under two seconds, prevents streak loss during busy moments, and stays discreet for meetings and Do Not Disturb.

Requirements

Biometric Passkey Unlock
"As a returning user under time pressure, I want to unlock with FaceID/TouchID instantly so that I can keep my streak without typing a password."
Description

Implement passkey-based biometric authentication to enable instant unlock via Face ID/Touch ID/Android Biometrics when launching from push, widget, or watch. Store credentials in Secure Enclave/Android Keystore with device binding and WebAuthn compatibility. Support fallback to device PIN when biometrics are unavailable, with configurable lockout after failed attempts. Maintain short-lived session tokens with silent refresh to prevent re-prompts during quick re-entries. Pre-warm the auth context on notification receipt to minimize round-trips and avoid UI flicker. Enforce anti-replay protections, key rotation, and secure keychain usage. Ensure OS coverage for iOS 16+/Android 10+/watchOS where applicable, with telemetry for success/failure reasons.

Acceptance Criteria
Instant Unlock from Push Deep Link
Given a signed deep-link push targeting a specific room and habit is received while the app is backgrounded and biometrics are available When the user taps the notification and completes biometric authentication Then the app unlocks and lands on the targeted room/habit within <= 2,000 ms p95 from foregrounding, exactly one biometric sheet is shown, no intermediate login screen is displayed, and the check-in control is enabled Given auth pre-warm is enabled and network is reachable When the push is received in background Then the server challenge and auth context are prepared, and upon tap the biometric prompt appears within <= 150 ms p95 without UI flicker (no show/dismiss cycles)
Widget Deep Link Biometric Unlock
Given an iOS 16+ or Android 10+ device with a StreakShare widget configured for a room/habit When the widget is tapped Then the biometric sheet appears within <= 300 ms p95 and, upon success, navigation to the correct room/habit completes within <= 2,000 ms p95 with exactly one navigation transition Given the widget is tapped while the device is locked When the device unlock flow completes via biometric/passcode Then the app navigates directly to the target without additional prompts if a valid session exists; otherwise exactly one biometric prompt is shown
Watch-Initiated Unlock Handoff
Given a watchOS complication or Wear OS tile is used to open a specific room/habit and the paired phone is nearby When the action is invoked Then the phone receives the deep link, presents a single biometric prompt within <= 500 ms p95 of handoff, and upon success navigates to the target within <= 2,500 ms p95 total Given the watch initiates unlock but the phone is unavailable When the unlock cannot proceed Then the watch displays a discreet error state and logs fail_reason=phone_unavailable without triggering audible alerts
Biometric Fallback and Lockout Policy
Given biometrics are unavailable, unenrolled, or the sensor is temporarily locked out When an unlock is initiated Then the OS-native device PIN/passcode fallback is offered within <= 1,000 ms Given consecutive biometric failures reach the configured threshold (default 5, configurable 1–10 via remote config) When another unlock attempt is made during the lockout window (default 30 s) Then biometrics are suppressed and only PIN/passcode is allowed, using OS-native messaging to indicate lockout Given a successful unlock via PIN/passcode When the app resumes Then a fresh session token is issued and biometrics remain enabled for subsequent attempts
Short-Lived Session with Silent Refresh
Given a successful biometric unlock has occurred When the user re-enters via push or widget within 5 minutes Then no biometric prompt is shown and a silent refresh renews the session token with latency <= 400 ms p95 Given the session token TTL is <= 15 minutes When re-entry occurs after expiry Then exactly one biometric prompt is shown prior to navigation Given network is temporarily unavailable during silent refresh When re-entry occurs within 5 minutes of last unlock Then a grace period up to 60 seconds allows navigation without a prompt and refresh retries in the background until connectivity resumes or grace elapses
Security Controls: Device Binding, Anti-Replay, and Key Rotation
Given passkey registration on a supported device When the credential is created Then the private key is generated in Secure Enclave (iOS) or Android Keystore (hardware-backed if available), is non-exportable, bound to the device, and the WebAuthn RP ID matches the expected domain; attestation indicates hardware-backed storage when available Given an authentication ceremony When the server issues a challenge Then the assertion includes a single-use, time-bound nonce (TTL <= 30 s, clock skew tolerance ±5 s) and any reused/expired challenge is rejected with 401 without issuing a session Given a rotation policy is active When rotation_required is signaled or a credential age exceeds 180 days Then the next successful unlock re-registers a new passkey and invalidates the old credential atomically, requiring at most one additional biometric prompt
Telemetry Coverage for Auth Outcomes
Given any unlock attempt from push, widget, or watch When the flow completes or aborts Then a single analytics event auth_attempt is emitted within 1 second with fields: surface (push|widget|watch), platform (iOS|Android|watchOS), os_version, app_version, device_model, result (success|fail|cancel), fail_reason (biometric_unavailable|not_enrolled|lockout|user_cancel|timeout|device_not_bound|replay_detected|attestation_failed|network_error|other), latency_ms (foreground→navigation), prewarm_used (bool), session_reused (bool) Given telemetry is enabled in production When observing a 7-day window Then >99% of attempts include all required fields and event duplication rate is <0.5%
Instant Deep-Link Router
"As a habit tracker, I want deep links to open directly to the right room and habit so that I can check in without navigating through the app."
Description

Provide a robust deep-link routing layer that opens directly to the target room and habit from notifications, widgets, and watch surfaces. Validate payloads (room_id, habit_id, timestamp, version), check permissions/membership, and handle missing or stale targets with graceful fallbacks (e.g., default daily habit or room list). Ensure idempotency to prevent duplicate check-ins on repeated taps. Support cross-device handoff from watch to phone when the action requires the handset. Maintain a versioned schema for link payloads with backward compatibility and secure signature verification to prevent tampering.

Acceptance Criteria
Direct Open from Push Notification
Given a push notification contains a valid deep-link payload with room_id, habit_id, timestamp (<=15 minutes old), and a recognized version And the user is a member with required permissions for the room and habit And the payload’s signature is valid When the user taps the notification Then the app launches and routes directly to the specified room and habit within 2 seconds of foregrounding And no intermediate home screen is shown And an analytics event "deeplink_open_success" with source=push, version, room_id, habit_id is recorded
Permission and Membership Enforcement
Given a deep-link payload targets a room/habit the user is not a member of or lacks permissions for When the link is opened from push, widget, or watch Then routing to the target is blocked And no check-in or other state mutation occurs And the user sees a non-sensitive error state indicating access is required And an analytics event "deeplink_access_denied" is recorded with source and target identifiers redacted
Idempotent Repeated Taps
Given a valid deep link to the same room/habit check-in is opened multiple times within 60 seconds across any surfaces When the router processes these openings Then at most one check-in event is created for the target habit and day And subsequent openings navigate to the existing target state or no-op without additional mutating requests And analytics contain exactly one "checkin_created" and one or more "deeplink_duplicate_blocked" events
Graceful Fallback for Missing or Stale Targets
Given a deep link has a room_id or habit_id that is deleted, or its timestamp is older than 15 minutes, or its version is unknown When the user opens the link Then the app does not crash And the user is routed to the default daily habit if present, else to the room list And a discreet notice indicates the original target is unavailable or expired And an analytics event "deeplink_fallback" is recorded with reason in {missing_target, stale_timestamp, unknown_version}
Cross-Device Handoff: Watch to Phone
Given the user taps a deep link on the watch that requires handset interaction (e.g., biometric auth or rich UI) When the action is initiated on the watch Then the phone receives a handoff within 1 second and brings the app to foreground And after FaceID/TouchID passkey succeeds, the app routes directly to the intended room and habit And if handset auth fails or is unavailable, no state mutation occurs and the watch shows a "Continue on iPhone" or equivalent status And analytics include "handoff_started" and either "handoff_completed" or "handoff_failed" with failure_reason
Versioned Payload Backward Compatibility
Given deep-link payloads of versions v1, v2, and v3 exist and the app currently supports v3 When the user opens each version Then v3 routes with full fidelity And v2 and v1 are mapped to the equivalent current targets using defined schema mapping rules without user-visible errors And an unknown future version vX triggers a graceful fallback without crash and records "version_unsupported" And in all cases, signature verification is performed before routing or fallback
Signature Verification and Tamper Rejection
Given a deep link with a missing, invalid, or mismatched signature is received from any surface When the user attempts to open it Then the router rejects the link before any navigation or state mutation occurs And the user sees a generic "link invalid" message without revealing payload details And an analytics event "deeplink_signature_invalid" is recorded And a security log entry is created with a correlation ID for investigation
Sub-2-Second Launch SLA
"As a busy professional, I want the app to reach the check-in screen in under two seconds so that I can complete my commitment between meetings without delay."
Description

Guarantee end-to-end time from entry action to check-in-ready screen under 2.0 seconds p95 on supported devices (e.g., iPhone 12+/Pixel 5+). Optimize cold and warm starts via lazy loading of non-critical modules, deferring analytics initialization, and prefetching room/habit metadata from the push payload. Use cached last-known state and optimistic rendering to show the target screen immediately, with background hydration. Add precise instrumentation (start/stop markers and trace spans) and enforce CI performance gates for p50/p95 thresholds by surface (push/widget/watch). Provide degraded modes (skeleton UI, deferred reactions) if the SLA risks breach.

Acceptance Criteria
Push Tap to Check-In-Ready SLA (Supported Devices)
Given a supported device (iPhone 12+/iOS 16+ or Pixel 5+/Android 13+), a valid deep-link push with roomId and habitId, and enrolled biometrics When the user taps the push notification Then the time from OS notification open to the check-in-ready screen (primary action enabled, correct room and habit visible) is ≤ 2.0s at p95 and ≤ 1.2s at p50 across ≥ 200 launches per device/OS And biometric unlock is included within the end-to-end budget; the FaceID/TouchID prompt appears ≤ 300ms from tap and does not cause p95 to exceed 2.0s And each run records start/stop markers and a duration in milliseconds for the push surface
Home Widget Tap to Check-In-Ready SLA
Given a supported device with the StreakShare widget configured and cached last-known state present When the user taps the widget targeted to a specific room/habit Then the check-in-ready screen is interactive and correctly deep-linked within ≤ 2.0s at p95 and ≤ 1.0s at p50 across ≥ 200 launches per device/OS And non-critical modules (analytics, reactions) load after first interaction and do not block TTI; first paint of skeleton appears ≤ 300ms from tap And each run records start/stop markers and a duration in milliseconds for the widget surface
Watch Quick Action to Check-In-Ready SLA
Given an Apple Watch Series 6+ or Wear OS watch paired to a supported phone within BLE range and the watch action configured When the user triggers the watch action to open the target room/habit Then a check-in-ready surface (watch app or handed-off phone app) is fully interactive within ≤ 2.0s at p95 and ≤ 1.3s at p50 across ≥ 200 launches per watch/OS And if handoff to phone occurs, the phone biometric prompt appears ≤ 400ms from action and the deep link is preserved And each run records start/stop markers and a duration in milliseconds for the watch surface
Cold vs Warm Start Performance Budgets
Given cold start (app not resident) and warm start (app in memory) conditions on supported devices When launching via push, widget, or watch Then cold start meets ≤ 2.0s p95 and ≤ 1.4s p50 to check-in-ready; warm start meets ≤ 1.6s p95 and ≤ 0.9s p50 And lazy loading defers non-critical modules; analytics initialization begins only after check-in-ready, verified by traces showing analytics start after TTI
Instrumentation Coverage and CI Gates
Given performance instrumentation with named start/stop markers per surface (push, widget, watch) and trace spans for cold vs warm starts When synthetic benchmarks run in CI and nightly on-device farms Then ≥ 99% of launches emit valid durations; marker-to-video correlation error is ≤ ±50ms on sample runs And CI gates fail the build if any surface’s p95 exceeds 2.0s or p50 exceeds its budget on supported devices And trend dashboards display p50/p95 by surface and start type for the last 30 days
Degraded Mode and Offline Readiness
Given network latency ≥ 400ms, partial outage, or metadata cache miss When launching via any surface Then a skeleton UI appears ≤ 300ms from entry; optimistic room/habit header from payload or cache renders ≤ 500ms And user can perform a one-tap check-in using cached state; the event queues offline and syncs within 5 minutes of connectivity recovery And if hydration of reactions/participants exceeds 1.5s, those elements defer without blocking interaction, and an unobtrusive loading indicator is shown
One-Tap Check-In Prime
"As a daily habit keeper, I want the check-in action to be ready and reliable on arrival so that I can confirm with a single tap and move on."
Description

Auto-focus the target habit on arrival with a large, accessible single-tap confirmation control. Pre-validate check-in rules (time windows, grace periods, streak protection) to avoid blocking prompts. Apply optimistic UI updates with subtle haptic feedback and sync to server in the background; queue offline intents with retry/backoff. Provide a 5-second undo option and accidental-tap protection on lock screen surfaces. Respect room privacy settings, deferring social broadcasts until confirmation and policy checks succeed. Ensure accessibility compliance (VoiceOver labels, contrast, hit area).

Acceptance Criteria
Auto-Focused One-Tap Check-In on Arrival
Given the user launches StreakShare via deep link (push, widget, or watch) targeting a specific habit in a room When the app unlocks via FaceID/TouchID and foregrounds Then the target habit is auto-focused and scrolled into view without additional taps, and the primary one-tap check-in control is visible and tappable within 2.0 seconds of the user’s initial trigger And no modal dialogs or blocking prompts are shown before the control becomes usable
Rule Pre-Validation to Prevent Blocking Prompts
Given the target habit has defined time windows, grace periods, and streak protection rules When the check-in control is rendered on arrival Then eligibility is pre-evaluated client-side and reflected in the control state And if eligible, the control is enabled and tapping it will not produce any blocking prompt due to those rules And if ineligible, the control is disabled and an inline, accessible message explains why and the next eligible time And no modal dialogs are displayed for rule failures at this stage
Optimistic UI with Haptic and Background Sync
Given the user is eligible to check in and the device has connectivity When the user taps the primary check-in control Then the check-in state updates optimistically within 150 ms, the visible streak increments, and a subtle haptic feedback fires And a background request with a client-generated idempotency key is sent to the server while the control indicates syncing And on server success, the syncing indicator clears without additional user action And on server rejection or validation failure, the UI reverts within 100 ms and a non-blocking error banner explains the reason, keeping the user in context And repeated taps within 1 second do not create duplicate submissions due to idempotency
Offline Queue with Ordered Retry and Backoff
Given the device is offline or the network request times out When the user taps the enabled check-in control Then the intent is stored in an offline queue with timestamp and idempotency key, and the UI shows a queued state while maintaining the optimistic check-in And queued intents for the same habit are preserved in order When connectivity resumes Then the client retries with exponential backoff starting at 2 s, doubling up to 5 min between attempts, for up to 8 attempts or until success And on success, the queued state clears without additional user action And on final failure, the optimistic state is reverted and a non-blocking notification provides a retry option
Undo Window and Accidental-Tap Protection on Lock Surfaces
Given a check-in is applied optimistically When the post-action state is displayed Then an Undo affordance is shown for 5 seconds and is accessible via tap or assistive technologies And if Undo is activated within 5 seconds, the local state is reverted and any pending network call is canceled; if already confirmed server-side, a compensating request reverts the check-in Given the interaction originates from a lock screen widget or watch surface When the user initiates a check-in Then a deliberate gesture (press-and-hold for ≥600 ms or double-tap within 800 ms) is required to commit And a single accidental tap does not commit the check-in
Privacy Respect and Deferred Social Broadcasts
Given the room has privacy settings or policy checks for check-ins When a check-in is initiated Then no social broadcasts (feed updates, reactions, or push notifications) are emitted until the server confirms the check-in and all policy checks succeed And if policy checks fail or the server rejects the check-in, no broadcast is emitted and the local optimistic state is reverted with a non-blocking notice And after confirmation, only authorized room members can view the check-in per room permissions
Accessibility Compliance for Check-In and Undo
Given a user relies on assistive technologies (VoiceOver/TalkBack) When the check-in screen loads Then the primary check-in control and Undo affordance expose correct accessibility roles, labels, states, and hints, and the focus order lands on the check-in control after arrival Given WCAG contrast requirements Then the check-in control, state indicators, and associated text meet contrast ratio ≥ 4.5:1 Given touch target guidelines Then the check-in control hit area is at least 44×44 pt (iOS) or 48×48 dp (Android) Given haptics are unavailable or disabled Then equivalent audible and/or visual feedback is presented for state changes Given reduced motion settings are enabled Then motion animations (e.g., press-and-hold progress) are minimized or replaced
Quiet Mode & DND Compliance
"As someone in DND or a meeting, I want the flow to be quiet and unobtrusive so that I don’t disturb others or draw attention to my check-in."
Description

Ensure the unlock and check-in flow remains discreet during meetings and Focus modes. Honor system DND/Focus by suppressing sounds, minimizing haptics, and calming animations. Add a user setting for Stealth Check-In that redacts celebratory effects and defers social reactions until focus ends. Redact habit names in notification previews when privacy is enabled and avoid waking the screen to full brightness. Use Time Sensitive notifications only when permitted; otherwise deliver silently. Verify behavior across iOS/Android focus states and watch vibration policies.

Acceptance Criteria
Silent Pop-In During DND/Focus
Given the device is in an active DND/Focus mode and the user launches StreakShare via push, widget, or watch deep link and authenticates with FaceID/TouchID passkey When the app opens the targeted room and habit and the user performs a one-tap check-in Then no audible sound is played, haptic feedback is suppressed or limited to OS-permitted silent taps, UI animations are reduced to a single fade ≤300 ms with no confetti, and the screen does not flash And the screen brightness does not increase beyond the current system level And the entire unlock-to-check-in completes in ≤2 seconds And the app does not request or attempt to override system DND/Focus
Stealth Check-In Defers Social Reactions
Given the user has enabled Stealth Check-In in settings and a system Focus/DND mode is active When the user completes a check-in Then celebratory visuals, sounds, and confetti are not rendered, and only a subtle success indicator is shown And no real-time reactions (emojis, comments, pings) are emitted to other users during the active Focus window And queued reactions are posted automatically within 60 seconds after Focus ends, preserving original timestamps and order And the reaction queue persists across app restarts during the Focus period
Notification Preview Privacy Redaction
Given the user has enabled Notification Privacy in StreakShare and system notification previews are set to When Locked or Never When a StreakShare notification (reminder, reaction, check-in confirmation) is delivered to lock screen, notification center, or wearable Then the notification title/body do not include habit names, room names, or user names and instead show a generic label such as StreakShare update And tapping the notification deep links to the correct room and habit after authentication And long-press/expand does not reveal redacted details unless the device is unlocked And wearable complications/tiles display only generic text while locked
Time Sensitive/High Priority Permission Gating
Given the user has not granted Time Sensitive (iOS) or High Priority (Android) notification permission When StreakShare delivers a check-in reminder during DND/Focus Then the notification is delivered silently without sound or vibration and does not break Focus/DND And the notification category is not marked Time Sensitive/High Priority Given the user has granted the permission and enabled Time Sensitive in app settings When StreakShare delivers a check-in reminder during DND/Focus Then it is marked appropriately (Time Sensitive/High Priority) and behaves per OS policy And an audit log records the category used and permission state
No Full-Brightness Wake On Pop-In
Given the device screen is off or dimmed due to DND/Focus or ambient mode When the user triggers Pop-In Unlock via push, widget, or watch deep link Then StreakShare respects current system brightness and does not increase brightness or wake to full-screen beyond OS defaults And the UI appears in a dimmed state consistent with the system And no camera flashlight or attention-grabbing visual effects are triggered
Watch Silent Policy Compliance
Given an Apple Watch in Silent or Theater mode or a Wear OS watch with DND enabled When a user receives a StreakShare notification or performs a watch check-in Then no sounds play, haptics are suppressed or reduced to the OS-minimum per policy, and no repeated vibrations occur And celebratory animations are replaced with a subtle checkmark limited to ≤300 ms And social reactions are deferred until the paired phone/watch exits Focus/DND
Cross-Platform Focus State Parity
Given supported platforms iOS 16+, Android 12+, watchOS 9+, and Wear OS 3+ When executing the DND/Focus scenarios above under Focus On and Focus Off states Then behavior is functionally equivalent across platforms: no sound, minimized haptics, subdued animations, privacy redaction, permission gating, and brightness control And automated tests cover at least one case per platform and focus state with 100% pass rate in CI And manual QA verifies on at least two devices per platform with results documented
Multi-Surface Entry Points
"As a smartwatch and lock screen user, I want to start my check-in from notifications, widgets, or my watch so that I can act quickly from wherever I am."
Description

Deliver consistent entry points across push notifications, Lock/Home Screen widgets, and watchOS/Android Wear tiles/complications that deep-link into the check-in flow. Implement actionable notification buttons (e.g., Check In) with required permissions and capability detection by OS version. Provide widget configuration to pin a specific habit/room and support glanceable streak state. Ensure coherent copy and iconography across surfaces and handle first-time authorization onboarding gracefully, including cross-app prompts.

Acceptance Criteria
Deep Link from Push Notification to Habit Check-In
Given a push notification targets habit H in room R, When the user taps the notification body, Then the app unlocks via FaceID/TouchID if configured and navigates directly to H’s check-in screen in R within 2 seconds of tap. Given the deep link context includes H and R, When the app opens, Then the correct habit and room are preselected and the one-tap check-in control is focused and actionable. Given H or R no longer exists or the user lacks access, When the user taps the notification, Then the app opens to the Home screen with a non-blocking message explaining the missing context and no crash occurs.
Actionable Push: In-Notification Check In
Given the device OS supports actionable notifications and required permissions are granted, When the user taps the "Check In" action on a habit notification, Then the check-in for that habit is recorded and a success confirmation is shown without launching the full app. Given biometrics/passkey is required to confirm check-ins, When the user taps "Check In," Then the system biometric prompt is shown inline and the action completes only upon successful authentication; on failure, no check-in is recorded. Given the OS version or permission state does not support actions, When the notification is delivered, Then the "Check In" action is hidden and tapping the body opens the deep link to the check-in screen instead. Given the user taps the "Check In" action multiple times within 10 seconds, When the backend receives duplicate requests, Then only one check-in is persisted (idempotent).
Lock/Home Screen Widget: Pin Habit with Glanceable Streak
Given the user adds the StreakShare widget, When they configure it, Then they can select a specific habit and room from a list and the selection persists across device restarts. Given the widget is configured for habit H, When the user views the widget, Then it displays H’s current streak count and today’s completion state (Done/Not Done) and updates within 1 minute after a check-in. Given the user taps the widget, When the app opens, Then it deep-links to H’s check-in screen within 2 seconds. Given the user has no habits, When they attempt to configure the widget, Then the widget shows a call-to-action to create a habit and tapping it opens the habit creation flow.
watchOS/WearOS Tile or Complication Deep Link
Given the user installs the watch app/tile and selects habit H, When they tap the tile/complication, Then it opens the check-in UI for H on the watch within 2 seconds. Given the watch cannot complete check-in offline, When the user taps to check in without connectivity, Then the UI queues the check-in and syncs automatically within 60 seconds of connectivity returning, showing final success state. Given the watch app is not installed or the tile requires the phone, When the user taps the tile, Then the watch prompts to open on the phone and the phone app opens directly to H’s check-in screen.
First-Time Permission and Authorization Onboarding
Given notifications permission is not granted, When the user attempts to enable actionable notifications or receives a push, Then the app shows an in-app explainer followed by the system permission prompt; if denied, actionable buttons are suppressed and the user sees a non-blocking reminder to enable later. Given biometrics/passkey is not set up, When the user launches via any surface, Then the app routes to a lightweight biometric/passkey setup flow and, upon completion, returns the user to the original deep-linked destination. Given cross-app authorization is required from a watch surface, When the user initiates check-in on the watch, Then a secure prompt appears on the phone to authenticate, and completion status is reflected on the watch within 5 seconds after auth.
Capability Detection and Graceful Degradation by OS Version
Given a device/OS combination lacks support for actionable notifications/widgets/tiles, When StreakShare is installed, Then unsupported surfaces or actions are not presented, and supported alternatives are shown instead without errors. Given OS versions with the relevant capabilities (e.g., iOS 16+ / Android 12+), When the app configures notifications and widgets, Then it registers the appropriate categories/intents and only requests permissions that are available on that OS. Given a deep link is opened on an unsupported app version, When the app receives the intent, Then it routes to the nearest valid destination (e.g., Room overview) and logs the degradation for telemetry.
Coherent Copy and Iconography Across Surfaces
Given any surface (push, widget, watch, app), When presenting the primary action, Then the label is exactly "Check In" in English and localizes consistently for other supported locales using the same string key. Given the action icon is rendered on push, widget, and watch, When viewed in light and dark modes, Then the glyph matches the design spec and meets minimum contrast guidelines (WCAG AA) without clipping or truncation. Given copy length constraints on each surface, When strings exceed the maximum display length, Then they are truncated with an ellipsis without breaking critical information such as the habit name.
Offline & Failure Fallbacks
"As a user with spotty connectivity, I want the app to handle errors and offline moments gracefully so that my check-ins still count when service returns."
Description

Build resilient handling for offline states, expired/invalid deep links, revoked membership, and authentication failures. Capture user intent locally with a signed nonce and timestamp to allow delayed check-in within policy windows, resolving conflicts server-side to prevent duplicates. Provide clear, lightweight error toasts with retry options and an enrollment path when passkeys/biometrics are not set up. Implement exponential backoff with jitter, and structured error codes for monitoring and support triage while preserving user privacy.

Acceptance Criteria
Offline Local Capture with Signed Nonce for Deferred Check-In
Given the user launches StreakShare via deep link to a specific habit room while the device is offline or the check-in request exceeds the client timeout (default 5s) When the user successfully authenticates with passkey/biometric Then the app generates a 128-bit cryptographically secure random nonce and an RFC3339 UTC timestamp with millisecond precision And signs {nonce,timestamp,habitId,roomId,action:"checkin"} using an app-scoped key in the secure enclave/keystore producing a verifiable signature And stores the payload encrypted at rest with status "queued" and an idempotency key equal to the nonce And shows a discreet toast "Check-in queued" within 300ms without sound or vibration And prevents a second queued item for the same tap by deduplicating on idempotency key
Deferred Sync, Idempotency, and Conflict Resolution
Given connectivity is restored within the server policy window for delayed check-ins When the client begins syncing queued check-ins Then it retries using exponential backoff with jitter (initial 1s, factor 2, max delay 60s, ±20% jitter) until success or max 10 attempts per item And it retries on network errors and 5xx, and does not retry on non-retryable 4xx (401,403,422 POLICY_WINDOW_EXPIRED) And the server validates the signature and creates at most one check-in per idempotency key, anchoring effectiveAt to the signed timestamp And if a check-in already exists for the same habit overlapping the timestamp, the server resolves by keeping a single record and returns CONFLICT_DUPLICATE with the surviving check-in id And on success the client marks the item "posted" and shows "Check-in posted"; on terminal failure it marks "expired" and shows "Queued check-in expired" with actions "Open app" and "Dismiss"
Expired or Invalid Deep Link Recovery
Given the app is opened by a deep link that is expired, has an invalid signature, or references a missing habit/room When the link is validated client-side and server-side Then the app does not attempt a check-in and does not crash And it routes the user to My Rooms as a safe fallback within 500ms And it shows a discreet toast "Link invalid or expired" for ≤3s with a "Browse rooms" action And it records a structured error code (LINK_INVALID or LINK_EXPIRED) for telemetry without PII
Revoked Membership Access Denial
Given the deep link targets a room where the user’s membership is revoked or pending removal When authorization is checked and the server returns 403 Then no local intent is queued and no check-in is created And a discreet toast "Access revoked" is shown for ≤3s with actions "Request access" and "Dismiss" And tapping "Request access" opens the membership request flow in-app And telemetry records MEMBERSHIP_REVOKED_403 without user PII
Authentication Failure and Passkey/Biometric Enrollment Path
Given the user invokes a one-tap check-in and passkey/biometric authentication is required When authentication fails due to not enrolled Then an enrollment sheet is presented within 500ms with actions "Set up now" and "Not now" And choosing "Set up now" launches the OS enrollment flow and returns to complete the original deep link if finished within 30s When authentication fails due to transient error or user cancel Then show a discreet toast "Authentication failed" with a "Retry" action; up to 3 retries are allowed before requiring full app unlock And no check-in intent is queued on authentication failure
Error Toast UX and Retry CTA (Discreet/DND-Safe)
Given any recoverable failure (network, server 5xx, link invalid, auth transient) occurs while the device may be in Do Not Disturb or a calendar meeting When the toast is displayed Then it uses no sound, no vibration, max 2 lines, and auto-dismisses within 3s And the toast includes a single "Retry" action that re-attempts the last operation immediately while respecting the backoff schedule And toast text contains no room names, habit titles, email, or user names
Structured Error Codes and Privacy-Safe Telemetry
Given any failure, denial, or terminal outcome is encountered When logging telemetry and exposing support data Then an error code matching the regex ^[A-Z]{2,}_[A-Z0-9_]{2,}(_\d{3})?$ is attached (e.g., AUTH_NOT_ENROLLED_401, LINK_EXPIRED_400, NET_TIMEOUT) And logs include only {errorCode, appVersion, platform, httpStatus, timestamp, anonymizedUserId, region} and exclude PII (email, displayName, room name, habit title, biometric raw data) And the error code is shown in the support share sheet but not in user-facing toasts

Scan-to-Sign

Sign in on any new phone, tablet, or web session by scanning a QR with a device that’s already signed in. Your phone approves the passkey request—no passwords, codes, or email loops. Cross-device access becomes instant and typo-free, perfect for hopping between work and home setups.

Requirements

Rotating QR Challenge on Login
"As a returning user with an existing signed-in phone, I want a scannable QR challenge on the new device’s login screen so that I can sign in instantly without typing passwords or codes."
Description

Display a single-use, time-bound QR code on StreakShare’s sign-in screens for new sessions across web, tablets, and phones. The QR encodes a server-issued challenge (nonce and challenge ID) tied to the unauthenticated session and rotates at a fixed interval (e.g., 30 seconds) to prevent reuse and replay. The page should poll the backend via SSE or WebSocket to detect approval in real time and seamlessly transition to an authenticated session upon success. Expired or consumed challenges are immediately invalidated. The UI clearly instructs users to scan with a device that’s already signed in, surfaces countdown/expiry states, and gracefully handles slow networks and refreshes. This requirement eliminates password entry on new devices, reduces typos and friction, and integrates with the existing auth service while preserving CSRF/origin protections and content security policies.

Acceptance Criteria
Happy Path: Approve within validity window for instant sign-in
Given an unauthenticated session loads the login screen and a server-issued QR challenge with a 30s validity and visible countdown, When a device that is already signed in scans the QR and the user approves within the remaining validity window, Then the backend marks the challenge as consumed, binds it to the requesting session, and emits an approval event, And Then the login screen receives the approval via SSE/WebSocket within 2s of approval and transitions to an authenticated session without a full page reload, And Then the success state replaces the QR and a session cookie is set with HttpOnly, Secure, and SameSite=Lax (or stricter), And Then the consumed challenge cannot be used to authenticate any session again.
QR Rotation and Countdown UX
Given the login screen displays a QR challenge with a 30s countdown, When 30s elapse without approval, Then within 500ms a new QR challenge replaces it with a reset 30s countdown and the prior challenge is marked expired server-side, And Then scanning the expired QR returns an "expired" result on the scanning device and the login screen remains unauthenticated, And Then the countdown updates at least once per second and visually/semantically indicates impending expiry in the last 5s.
Single-Use and Replay Protection
Given a challenge is expired or has been consumed by a successful approval, When any client attempts to approve it again or present it for a different unauthenticated session, Then the server rejects the attempt (401/410) and no session becomes authenticated, And Then replay attempts from a different IP/device or after QR rotation are consistently rejected, And Then a user-facing message indicates the code is no longer valid and prompts to scan the latest QR.
Session Binding, Origin, and CSRF Enforcement
Given each QR challenge is bound to a specific unauthenticated session and origin, When an approval arrives with a mismatched session ID, missing/invalid CSRF token, or disallowed Origin/Referer, Then authentication is rejected with 403 and the login page remains unauthenticated, And Then the SSE/WebSocket connection is same-origin over HTTPS and only accepts session-bound events, And Then the login page enforces the configured Content-Security-Policy with no inline script/eval violations and no mixed-content warnings.
Network Resilience and Refresh Handling
Given the login page is connected to the backend via SSE or WebSocket, When the connection drops or latency exceeds 1s, Then a non-blocking "Reconnecting…" indicator appears within 1s and the client retries with exponential backoff up to 30s, And Then on reconnection the client refreshes the current challenge state from the server without duplicating approvals, When the user refreshes the page, Then the prior pending challenge is invalidated server-side within 1s and a new challenge is issued, And Then approvals for the invalidated challenge do not authenticate the refreshed session.
Concurrent Sessions and Scanning Device State
Given two different unauthenticated sessions display distinct QR challenges, When one challenge is approved from a signed-in device, Then only the corresponding session transitions to authenticated and the other remains on the login screen, When a device that is not currently signed in scans the QR, Then approval is blocked and both devices display instructions to sign in on the scanning device first, And Then if the scanning device's signed-in account is not permitted for the requesting session context (if applicable), authentication is rejected with an explicit message.
Throttling and Audit Logging
Given normal and abusive usage patterns, When more than 10 scan attempts or 5 approvals are initiated within 1 minute for the same unauthenticated session or scanning device, Then further attempts are throttled with 429 and the UI displays a "Too many attempts, please wait" message while QR rotation continues, And Then all outcomes (success, expired, replayed, forbidden, throttled) are written to audit logs with challenge_id, session_id, timestamp, and reason without storing raw secrets, And Then no throttling state persists longer than 15 minutes after the last event.
Biometric Passkey Approval on Signed-in Device
"As a security-conscious user, I want to approve sign-in requests with my phone’s biometrics so that only I can authorize new devices and I can see exactly what I’m approving."
Description

Enable the StreakShare app on an already authenticated device to receive the QR challenge, display the requesting device’s details (browser, OS, approximate location, and time), and require a biometric or device PIN passkey confirmation before approval. The app signs the server challenge using the platform authenticator (WebAuthn/passkey) and submits the signed assertion with explicit user consent (approve/deny). The approval screen includes clear risk cues, a visible timer for challenge expiry, and a one-tap deny option. This requirement ensures strong, phishing-resistant authentication with clear user consent and integrates with the existing identity service and push/deep-link handling.

Acceptance Criteria
Biometric Approval Within Validity Window
Given the user is signed in on Device A with a registered passkey and initiates a QR login on Device B And Device A displays an approval screen with the requesting device browser, OS, approximate city/region, and request time And a countdown timer shows the remaining validity (initial TTL 60 seconds) When the user authenticates via biometric on Device A and taps Approve before the timer expires Then the app signs the server-provided challenge using the platform authenticator with userVerification=required and rpId=streakshare.app And submits the signed assertion to the server over TLS And Device B becomes authenticated within 3 seconds of approval And Device A shows a success confirmation with the approved device/browser and timestamp
One-Tap Deny and Immediate Revocation
Given the approval screen is displayed on Device A for a pending QR login from Device B When the user taps Deny Then no biometric or PIN is requested And the server immediately invalidates the pending challenge And Device B shows "Request denied" within 3 seconds and remains unauthenticated And Device A shows a denial confirmation and offers "Report suspicious" action And an audit log entry is recorded with requestId, outcome=denied, device details, and timestamp
Challenge Expiry Handling
Given a pending QR login request is displayed on Device A with a countdown When the countdown reaches zero or the server marks the challenge expired Then the Approve action is disabled and visually indicated as expired And attempting to approve after expiry is blocked client-side and rejected server-side with an "expired" error And Device B shows "Request expired" within 3 seconds and prompts to rescan And the expired request disappears from Device A within 5 seconds or on screen refresh
Fallback to Device PIN When Biometrics Unavailable
Given biometrics are unavailable or fail 3 consecutive attempts on Device A When the user opts to continue with device PIN/passcode Then the app requests OS-level user verification via PIN and proceeds only on success And on successful PIN verification, the challenge is signed and submitted as in the biometric flow And if the user cancels or fails PIN verification, no approval is sent and the request remains pending until expiry or deny
Risk Cues and Device Detail Disclosure
Given Device A shows an approval screen for a QR login request Then the screen displays the requesting device's browser, OS, approximate city/region derived from IP, and request time in the user's local timezone And a persistent risk banner appears when the requesting device is new, the location differs by more than 100 km from the user's last approved location, or multiple requests are received within 2 minutes And the Deny action is visible above the fold on all screen sizes And tapping the risk info reveals a brief explanation without leaving the approval flow
Push/Deep-Link Delivery and App State Handling
Given a QR login request is initiated on Device B for the user's account When a push notification is delivered to Device A and the user taps it Then the app opens directly to the approval screen within 2 seconds, even if the app was closed And if push delivery fails, opening a deep link (streakshare://approve?requestId=...) brings the user to the same approval screen And if Device A has no network connectivity, the approval screen shows an offline error state with Retry and Deny options, and no approval is sent
Security and Consent Verification
Given the user taps Approve after successful biometric/PIN verification Then the signed assertion includes the correct challenge, clientDataJSON origin for https://streakshare.app, and is bound to the user's passkey credential id And the server verifies rpId, user verification, signature, and challenge freshness before issuing a session to Device B And no biometric data leaves Device A; only the WebAuthn assertion is transmitted And an audit log captures consent=approved, approver device id, requesting device details, IP, and timestamps for both devices
Encrypted Session Handoff and Device Binding
"As a frequent multi-device user, I want the approved scan to securely hand off my session to the new device so that I can start using StreakShare immediately without extra steps."
Description

Implement a secure handoff protocol that converts an approved challenge into an authenticated session on the new device. Upon valid passkey assertion and consent, the backend issues session/refresh tokens to the new device after verifying origin, nonce, and replay protection. The handoff uses ephemeral keys to encrypt the bootstrap token, binds the session to the target device’s fingerprint (user agent, platform signals), and records the device in the user’s trusted devices list. The flow strictly prevents replay, enforces single-use, and logs all transitions. Successful handoff results in automatic entry to StreakShare with the correct account while enabling future one-tap approvals and device management.

Acceptance Criteria
QR Scan Handoff Creates Authenticated Session
Given a signed-in device scans a valid QR challenge shown on a new device And the signed-in device receives a passkey approval prompt for the user’s account When the user approves and the passkey assertion verifies successfully Then the backend issues a session token and refresh token to the new device And the new device enters the StreakShare app as the correct account within 3 seconds of approval And the signed-in device remains authenticated and no password, email, or code is requested on either device
Bootstrap Token Encrypted with Ephemeral Keys
Given a handoff is approved and a bootstrap token is generated for the new device When the token is transmitted between backend and the new device Then it is protected using ephemeral ECDH key agreement with authenticated encryption And the token is never present in plaintext on the wire or in logs And tampering with the ciphertext causes decryption failure and no tokens are issued And ephemeral private keys are destroyed immediately after use
Single-Use Challenge and Replay Protection
Given a QR challenge is created with a nonce tied to the new device When the challenge is redeemed once Then any subsequent attempt to redeem the same challenge or nonce is rejected and no tokens are issued And the challenge expires after 120 seconds or upon first successful redemption, whichever occurs first And attempts to replay a captured approval from another device or session are rejected due to device-bound keys and nonce mismatch And all rejections are logged with reason code replay_blocked
Device Binding and Trusted Devices List Update
Given a handoff completes successfully When the new device first uses the issued refresh token Then the device is bound to a fingerprint consisting of user agent, platform signals, and key material hash And the device appears in the user’s Trusted Devices list with label, created_at, last_seen_at, and platform And removing this device from the list immediately revokes its session and refresh token and prevents further refresh And subsequent approvals can target this bound device without re-enrollment
Origin, Nonce, and Assertion Verification Before Token Issuance
Given an approval is initiated from a signed-in device When the backend validates the passkey assertion Then the assertion’s origin matches the allowlisted StreakShare app/web origins And the challenge nonce equals the one displayed on the new device and is unexpired And the signature verifies against the user’s registered credential And if any check fails, no tokens are issued and the user sees a non-destructive error
End-to-End Audit Trail of Handoff
Given a handoff is attempted When events occur across both devices and backend Then logs contain correlated entries for challenge_created, scan_received, approval_requested, approval_granted/denied, handoff_issued, device_bound, and failures with reason codes And logs store timestamps, user id, anonymized device identifiers, IPs, and correlation_id And logs contain no secrets, tokens, or plaintext bootstrap data And security audit queries can reconstruct the full flow using correlation_id within 24 hours of the event
Consent Denial or Timeout Blocks Handoff
Given a user scans a QR challenge When the user denies the approval or no response is received within 120 seconds Then the challenge is invalidated and cannot be reused And no session or refresh tokens are issued to the new device And both devices show a clear failure state with retry option And the event is logged with reason code user_denied or timeout
Cross-Platform Deep Link and Scan UX
"As a mobile user, I want scanning a QR to open the StreakShare app and guide me through approval quickly so that the process feels instant and reliable on my device."
Description

Provide a seamless cross-platform experience for initiating and completing Scan-to-Sign. The QR payload must resolve via universal links/app links to open the StreakShare app directly on iOS and Android; if the app is not installed, guide the user to install and resume the approval. Inside the app, include a dedicated scanner that requests camera permission with clear rationale, supports accessibility (screen readers, large text), and gives immediate feedback for expired or invalid codes. The UX targets sub-500 ms perceived latency from scan to approval, includes localized copy, and offers clear recovery guidance. This requirement ensures discoverability, speed, and reliability across device ecosystems and network conditions.

Acceptance Criteria
Deep link resolve, install fallback, and resume
Given the StreakShare app is installed on iOS or Android and a user scans a valid Scan-to-Sign QR, When the QR is recognized, Then the OS resolves the universal/app link to open StreakShare directly to the Approve Sign-in screen within 1.0 second and the request ID matches the QR payload. Given the StreakShare app is not installed, When the user scans a valid Scan-to-Sign QR, Then the user is routed to the correct App Store/Play Store listing within 2.0 seconds and an install-resume token tied to the request is persisted. Given the user completes installation from the store, When StreakShare is opened the first time within 30 minutes, Then the Approve Sign-in screen auto-opens with the original request preloaded within 2.0 seconds and the install-resume token is invalidated. Given any deep link failure (e.g., blocked interstitial), When the link cannot open the app, Then a mobile web fallback page loads with two CTAs: Open in App and Get the App, both of which route to the same resume-capable flow without more than one redirect between app and browser per attempt.
Dedicated scanner discoverability and reliability
Given a signed-in user is on Home, When they need to scan a QR to approve a sign-in, Then the Scan to Sign entry point is reachable within two taps and labeled consistently across iOS and Android. Given the user opens the in-app scanner, When the scanner view loads, Then the camera preview appears and begins decoding within 300 ms and provides a torch toggle on supported devices. Given a valid QR is in frame at 15–60 cm under 50–500 lux, When the code is steady for ≥100 ms, Then the decoder detects and parses the payload within 1.0 second at P95.
Camera permission rationale and denied flow
Given the user opens the in-app scanner for the first time, When StreakShare requires camera access, Then a pre-permission rationale sheet is shown with localized copy explaining purpose and privacy, followed by the OS permission prompt upon user consent. Given the user grants camera permission, When the OS prompt is accepted, Then the scanner activates within 300 ms and is ready to scan. Given the user denies camera permission, When the OS prompt is dismissed or denied, Then the scanner view shows a non-blocking state with Enable Camera (deep link to app settings), Learn More, and Cancel; and the app remains responsive. Given the user returns from Settings after enabling permission, When StreakShare regains focus, Then the scanner automatically activates within 300 ms without requiring app restart.
Expired or invalid QR feedback and recovery
Given the scanner decodes a QR with an expired or invalid payload, When validation occurs client-side, Then an error banner and haptic warning appear within 200 ms and screen readers announce the error. Given an expired code error is shown, When the user taps Try Again, Then the scanner resumes immediately; When the user taps Get New Code, Then a help sheet explains how to generate a new QR on the requesting device. Given a malformed payload is detected, When the user attempts to proceed, Then no network request is sent and the UI prevents progression with clear messaging and a Retry option.
Perceived latency from scan to approval
Given a valid QR is scanned and the user confirms approval if required, When the approval request is sent, Then the perceived time from decode to Approved confirmation is ≤ 500 ms at P95 on Wi‑Fi and ≤ 500 ms at P90 on LTE, measured by client telemetry. Given network RTT > 150 ms or transient packet loss ≤ 2%, When approval is in progress, Then the UI shows optimistic progress within 100 ms to maintain perceived responsiveness and never blocks input. Given the approval does not complete within 2.0 seconds, When the timeout elapses, Then a non-destructive error state appears with Retry and Switch Device options, and no more than one automatic background retry occurs.
Accessibility compliance for scanner and approval
Given a screen reader is active, When the scanner and approval screens are opened, Then all actionable elements have accessible names, roles, and hints; focus order follows visual order; and status changes (scanned, approved, error) are announced via live regions. Given large text/dynamic type up to 200% is enabled, When the scanner and approval screens render, Then critical controls remain visible and tappable, labels do not truncate essential information, and layouts do not overlap. Given high-contrast or dark mode is enabled, When the scanner and approval screens render, Then text and interactive elements meet WCAG 2.1 AA contrast (≥ 4.5:1 for text) and hit targets are ≥ 44×44 pt. Given haptics are available, When a code is recognized, approved, or rejected, Then distinct success and error haptic patterns play and can be disabled in Settings.
Localization and RTL support
Given the device locale is one of the supported locales (e.g., en, es, pt-BR, fr, de, ja), When deep link pages, scanner, permission rationale, errors, and recovery messages are shown, Then all copy is localized and fits within UI bounds without truncation at P95. Given an unsupported locale or a missing string, When the UI is displayed, Then content falls back to English without placeholder keys and logs a telemetry warning. Given a right-to-left locale (e.g., ar), When the scanner and approval screens render, Then layout mirrors appropriately, numeric content displays correctly, and primary controls remain discoverable and operable.
Risk Scoring, Rate Limits, and Admin Controls
"As a product owner, I want configurable risk checks and limits around Scan-to-Sign so that we can minimize abuse while keeping the experience fast for legitimate users."
Description

Add adaptive risk checks and operational controls to protect the Scan-to-Sign flow. Evaluate IP and geolocation deltas between devices, ASN reputation, device age, and time-of-day anomalies; require re-auth or add friction for high-risk events. Rate limit challenge issuance and approvals per account and per IP to deter abuse, and block known malicious origins. Provide admin/config flags for enabling the feature per platform, setting challenge TTL, adjusting risk thresholds, and toggling verbose security logging. This requirement reduces fraud and account takeovers while allowing operations to tune security posture without code changes.

Acceptance Criteria
Adaptive Risk Step-Up on Suspicious Scan Approval
Given risk thresholds configured: geo_delta_km_high=500; device_age_hours_high=24; time_of_day_anomaly_enabled=true; asn_reputation_blocklist=["HighRisk"] And a Scan-to-Sign approval attempt where one or more risk factors meet or exceed the configured thresholds When the risk engine computes a score >= configured_step_up_threshold Then the approval flow requires step-up re-auth on the approving device (biometric or passkey) And the approval is denied if step-up is not completed within 60 seconds And no session is established until step-up succeeds And a "risk_step_up_required" security event is recorded with request_id, risk_score, matched_factors, and platform And changes to configured thresholds in Admin Console take effect within 60 seconds of save
Rate Limiting Challenge Issuance per Account and IP
Given rate limits configured: per_account_challenges=5/10min; per_ip_challenges=20/10min; cooldown=60s And an actor attempts to initiate multiple Scan-to-Sign challenges When the number of challenges exceeds any configured limit within the window Then the new challenge is not created And the API returns 429 (or client shows "Rate limit exceeded") with retry_after equal to remaining window seconds And a "rate_limit_triggered" event is logged with scope (account|ip), limit, window, and request_id And challenge counters reset automatically at window expiry
Challenge Time-To-Live (TTL) Enforcement
Given challenge_ttl_seconds=60 And a QR challenge was created at T0 When an approval attempt arrives after T0+60s Then the approval is rejected as expired And the client is prompted to initiate a new scan And a "challenge_expired" event is logged with challenge_id, issued_at, expired_at, and request_id And expired challenges cannot be approved thereafter from any device
Blocking Known Malicious Origins
Given a blocklist configured with ip_ranges and asns including the request origin When a Scan-to-Sign challenge issuance or approval is attempted from the blocked origin Then the request is rejected And the API returns 403 (or client shows "Request blocked") And no challenge or notification is created or delivered And a "blocked_origin" event is logged with reason (ip|asn), origin value, and request_id
Admin Feature Flags per Platform
Given feature flags configured: scan_to_sign_enabled={ web:false, ios:true, android:true } When Scan-to-Sign is initiated on Web Then the initiation is blocked with a "Feature disabled" message and no challenge is created And when initiated on iOS or Android, the flow proceeds and security controls execute And toggling any platform flag in Admin Console takes effect within 60 seconds without deployment And all flag changes are auditable with actor_id, old_value, new_value, and timestamp
Verbose Security Logging Toggle
Given verbose_security_logging=true When a challenge is issued or an approval decision is made Then a log entry includes: request_id, user_id_hash, risk_score, risk_factors, decision, limits_applied, ip, asn, geo_country, device_age_hours, platform, timestamp And no secrets, tokens, or biometric data are logged And when verbose_security_logging=false, only request_id, decision, platform, and timestamp are logged And logging behavior changes within 60 seconds of toggling
Rate Limiting Approvals per Account and IP
Given rate limits configured: per_account_approvals=10/10min; per_ip_approvals=30/10min; cooldown=60s And multiple approval attempts are made from the same account or originating IP within the window When the number of approvals exceeds any configured limit Then further approvals are rejected And the approving device displays "Rate limit exceeded—try again later" And a "rate_limit_triggered" event is logged with scope (account|ip), limit, window, and request_id And approval limits do not affect unrelated accounts or IPs
Camera-less Pairing Code Fallback
"As a user without a working camera, I want a short pairing code I can enter on my signed-in phone so that I can still sign in on a new device without passwords."
Description

Offer a code-based pairing alternative when a camera is unavailable or the QR cannot be scanned. The new device displays a short-lived alphanumeric pairing code that represents the same challenge; the signed-in device’s app provides an input to enter the code and proceeds through the same passkey approval and handoff flow. Codes are single-use, have strict TTL (e.g., 60 seconds), and include attempt limits and lockouts. This ensures accessibility and reliability without reverting to passwords or email loops while maintaining security guarantees equivalent to QR scanning.

Acceptance Criteria
Camera Unavailable: Generate Single-Use Pairing Code
Given a new device cannot scan a QR or the user selects "Use code" in Scan-to-Sign When the fallback flow is initiated Then the new device displays a single-use 8-character uppercase alphanumeric code excluding 0,O,1,I and a visible 60-second countdown And the code is registered server-side as an active pairing challenge bound to the new device and expires exactly 60 seconds after issuance And only one active code exists per new device at any time
Valid Code Entry: Approve and Handoff Session
Given a signed-in device opens Scan-to-Sign and selects "Enter code" When the user enters a valid, unexpired code from the new device Then the code is marked consumed immediately and cannot be reused And the signed-in device prompts for passkey approval for the new device's challenge And upon successful passkey assertion, the new device receives session handoff within 2 seconds and transitions to the signed-in home And the code entry field accepts exactly 8 characters, auto-uppercases input, and rejects disallowed characters
Attempt Limits: Lockout After Repeated Invalid Entries
Given an active pairing challenge code When 5 invalid code submissions occur for that challenge within its 60-second lifetime across any signed-in devices Then further submissions for that challenge are blocked for 2 minutes and return "Too many attempts—try again shortly" And the lockout does not trigger passkey approval and does not consume the code And counters reset when the challenge expires or is cancelled And a global rate limit enforces a maximum of 10 invalid submissions per minute per account for code-based pairing
Expiration: Auto-Regenerate and Invalidate Old Code
Given a code is displayed on the new device When the countdown reaches 0 seconds Then the code is marked expired server-side and cannot be validated And the UI automatically generates and displays a new code within 1 second, invalidating the previous one And validations of an expired code return "expired" and do not count toward invalid-attempt limits And code regeneration is limited to 3 per minute per new device
Security Parity: Equivalent Guarantees to QR Flow
Given a code is validated and the user approves the passkey on the signed-in device When the server verifies the assertion Then the same challenge binding, origin checks, and attestation requirements as the QR flow are enforced And the resulting session on the new device has identical scopes and TTL as sessions established via QR And the code value is never stored in plaintext at rest; only a salted hash and metadata are retained until expiration And an audit log entry is recorded with method "code", challenge ID, device identifiers, IPs, and timestamps
Concurrency and Cancellation: Single Active Code and Revocation
Given an active code is displayed on the new device When the user cancels on either device or initiates a new code on the new device Then the active code is immediately revoked and becomes invalid And only one active code per new device is permitted at any time And if two signed-in devices submit the same valid code concurrently, only the first submission succeeds; subsequent ones return "consumed"
Audit Trail and Funnel Analytics
"As a user, I want to see my recent Scan-to-Sign activity and revoke any session I don’t recognize so that I can keep my account secure; as a PM, I want funnel metrics so that I can improve conversion."
Description

Capture a comprehensive audit trail and product analytics for the Scan-to-Sign flow. Log key events (challenge issued, scanned, approved/denied, expired, risk flagged, session created, device bound) with timestamps and minimal, privacy-safe metadata. Provide a user-visible recent sign-ins list with device, approximate location, and the ability to revoke sessions or remove trusted devices. Ship funnel metrics (QR view → scan → approve → session) to analytics to quantify drop-off and time-to-complete and surface KPIs in the admin dashboard. This requirement ensures compliance, supports incident response, and enables iterative UX optimization.

Acceptance Criteria
End-to-end Scan-to-Sign event logging
- Given a user completes a successful Scan-to-Sign flow, When the flow finishes, Then the audit trail contains events in order: challenge_issued, qr_scanned, approval_granted, session_created, device_bound, each with an ISO 8601 timestamp and a shared correlation_id. - Given a user denies the request, When the flow ends, Then approval_denied is recorded and no session_created or device_bound event exists for that correlation_id. - Given a challenge expires, When the expiry time is reached, Then challenge_expired is recorded and no session_created or device_bound event exists for that correlation_id. - Given a risk rule triggers during scan or approval, When the event occurs, Then risk_flagged is recorded with rule_id and severity. - Given any audit event is recorded, When metadata is persisted, Then only fields defined in the audit schema are stored (event_id, correlation_id, event_type, timestamp, device_descriptor, approx_location, outcome, optional risk fields).
Privacy-safe metadata enforcement
- Given an audit event is written, Then no PII (e.g., email, phone number, full IP address, exact GPS) is stored in audit or analytics payloads. - Given location is included, Then it is approximate (city/region level) and derived without storing raw IP or precise coordinates. - Given device information is stored, Then only non-unique descriptors (device type, OS, browser) and a randomized device_id are persisted. - Given an analytics payload is sent, Then it includes only anonymized identifiers (event_id/correlation_id) and excludes user identifiers and content data. - Given data validation runs, Then any payload containing disallowed fields is rejected and the rejection is logged as audit_schema_violation.
Recent sign-ins list and session/device control
- Given a signed-in user opens Recent Sign-ins, When data loads, Then the list shows at least the 10 most recent sign-in attempts with timestamp, device label, approximate location, and outcome (approved/denied/expired). - Given the user selects Revoke on an active session, When the action is confirmed, Then the session is terminated within 30 seconds and a revoke_session event is logged. - Given the user removes a trusted device, When the action is confirmed, Then the device is unbound, future Scan-to-Sign from that device requires approval, and device_unbound is logged. - Given a revoke or remove action completes, When the list is refreshed, Then the status reflects the change and a success confirmation is shown to the user. - Given any user-visible entry, Then it contains no PII beyond device label and approximate location.
Funnel metrics emission and reliability
- Given a user views the Scan-to-Sign QR, Then analytics event qr_view is sent within 2 seconds with a correlation_id unique to the flow. - Given the QR is scanned, Then analytics event qr_scanned is sent with the same correlation_id. - Given approval is granted or denied, Then analytics event approval_granted or approval_denied is sent with the same correlation_id. - Given a session is created, Then analytics event session_created is sent and includes time_to_complete_ms computed from qr_view to session_created. - Given temporary network loss, When connectivity is restored, Then buffered analytics events are retried and delivered at-least-once and are de-duplicated by event_id. - Given daily reconciliation, Then per-step counts between analytics and audit logs match within 1% or 50 events (whichever is greater).
Admin dashboard KPIs for Scan-to-Sign
- Given an admin selects a date range, When the dashboard loads, Then it displays total QR views, scans, approvals, sessions created, step conversion rates, and median time-to-complete for Scan-to-Sign. - Given the admin filters by platform (iOS/Android/Web), Then all KPIs and charts update to reflect the filter. - Given the admin drills into a KPI, Then a time series with daily granularity is shown for the selected metric and range. - Given validated analytics backfill, Then dashboard values for the range match the analytics store within 2%. - Given the dashboard is viewed, Then no user PII is displayed; only aggregates and anonymized identifiers are shown.
Audit log integrity, access control, and export
- Given audit logs are stored, Then they are append-only: no API or UI allows editing or deletion of existing records. - Given an event is created, Then it includes event_id (unique), timestamp with timezone, actor_type (user/system), and correlation_id to link the flow. - Given a privileged admin requests an export for a date range, Then CSV and JSON exports are generated within 60 seconds containing the selected fields and a checksum per record. - Given an unauthorized user attempts to view or export audit logs, Then access is denied and an audit_access_denied event is recorded. - Given an export file is generated, Then its checksum validates successfully upon download, indicating no tampering in transit.

One-Tap Enroll

Create a passkey during onboarding or your next sign-in with a single tap. No password to invent or remember—your device biometric becomes your key. Reduces drop-off, speeds first-run setup, and makes coming back after a lapse essentially frictionless.

Requirements

Passkey Enrollment via Biometric
"As a new StreakShare user, I want to enroll with my device biometric in one tap so that I can start tracking habits without creating or remembering a password."
Description

Implement a one-tap passkey enrollment flow using platform WebAuthn/FIDO2 so users can create a discoverable, device-bound credential with their biometric (Face ID/Touch ID/Android biometrics/Windows Hello). The flow triggers during onboarding and for returning passworded users at next sign-in, presenting a single tap and native biometric prompt, then registering the public key with the StreakShare backend. The backend must generate and validate a unique challenge, store the credential ID, public key, AAGUID, and metadata, and associate them with the user profile. The client should automatically detect platform capability, gracefully degrade if unsupported, and surface a clear success state that continues to the app without extra steps. Registration should default to resident keys with user verification required to enable username-less sign-in. Ensure the RPID is bound to the production domain/app package, support dev/stage environments via config, and prevent duplicate enrollment by deduping credentials per device. The outcome is a single-tap, sub-5-second enrollment that reduces drop-off in first run and sets up frictionless return access.

Acceptance Criteria
Onboarding: One-Tap Passkey Enrollment Success
Given a new user completes initial sign-up on a device with a supported platform authenticator and enrolled biometrics When the user taps the one-tap enroll CTA Then the app invokes WebAuthn create() with options including: discoverable/resident credential enabled, userVerification='required', valid rp.id for the current environment, and a server-issued, single-use challenge And the native biometric prompt is displayed And upon successful biometric verification, registration completes in ≤ 5 seconds at p95 from tap to success response And the backend verifies the challenge and attestation, persists credentialId, publicKey, AAGUID (if provided), and metadata associated with the user profile, and marks the credential as device-bound and discoverable And the client navigates directly into the app without additional steps
Next Sign-In: Prompt Passworded Users to Enroll Passkey
Given a returning user signs in with a password on a device that supports WebAuthn and has no passkey enrolled for this device When post-authentication completes Then the user is shown a single-tap passkey enroll prompt that is non-blocking When the user accepts and passes biometric verification Then a discoverable credential with userVerification='required' is registered and stored as per backend requirements within ≤ 5 seconds p95 And the session remains active and the user stays in-app When the user declines Then the app suppresses the prompt for the rest of the session and provides a clear path to enroll later
Platform Capability Detection and Graceful Degradation
Given a device/browser/app session When capability checks run (e.g., isUserVerifyingPlatformAuthenticatorAvailable and platform authenticator availability) Then the enroll CTA is enabled only if a platform authenticator and enrolled biometrics are available; otherwise it is disabled or hidden within 1 second and a clear fallback is shown And attempting enrollment on an unsupported device never triggers a biometric prompt and routes the user to alternative sign-in/setup without error And any timeouts for platform operations do not exceed 10 seconds and surface actionable messaging
Backend Challenge Generation, Validation, and Credential Persistence
Given a registration-start request When the server generates PublicKeyCredentialCreationOptions Then the challenge is cryptographically random, ≥ 32 bytes, unique per request, and set to expire in ≤ 5 minutes And rp.id and user identifiers match the current environment and signed-in user And excludeCredentials includes any existing credential IDs for this user and device (if known) When the server receives the registration response Then it validates origin/rpId, clientData, attestation/object, UV flag, and challenge freshness/single-use via FIDO2 verification And on success, persists credentialId, publicKey, AAGUID (if provided), authenticator metadata, and association to the user profile; on failure, returns a 4xx with a specific error code and does not persist
Discoverable Credential and User Verification Required
Given registration is initiated When create() is called Then options require a discoverable/resident credential and userVerification='required' And after success, navigator.credentials.get can authenticate for this RP without specifying allowCredentials (username-less), proving discoverability And the server verifies that the UV flag is present in the authenticator data for the registered credential
RPID and Environment Configuration Integrity
Given environment configuration (dev, stage, prod) When a registration is attempted Then rp.id exactly matches the configured relying party ID for that environment, and origin/app package/facet IDs align accordingly And credentials registered in one environment cannot be used in another (cross-environment assertions are rejected) And a deliberate rp.id mismatch causes registration to fail with a security error and no data persisted
Per-Device Deduplication and Idempotent Enrollment
Given a user already has a passkey enrolled on the current device When they attempt to enroll again Then excludeCredentials prevents creation of a duplicate credential where supported; otherwise the server detects the duplicate by credentialId/device fingerprint and performs an idempotent no-op And the user sees an immediate success state without additional prompts, and the total credential count for that device remains 1
Usernameless One-Tap Sign-In
"As a returning user, I want to sign in with a single biometric tap so that I can resume my streaks quickly without typing credentials."
Description

Enable passwordless, usernameless sign-in using passkeys so returning users can authenticate with a single tap and biometric verification. Implement WebAuthn get() with discoverable credentials (no allowCredentials list) to let the platform passkey selector handle account discovery. On success, the backend verifies the assertion (challenge, RP ID, signature, counter) and issues session tokens promptly, targeting a total authentication time under 2 seconds. The client should default to passkey if available, fall back to other options only on explicit failure, and show error states that encourage retry or recovery. Support multiple credentials per user, handle cloned/rotating authenticators via counter checks, and invalidate sessions appropriately on logout. Integrate with existing session creation, refresh, and device telemetry to maintain trust and reduce re-auth prompts during daily check-ins.

Acceptance Criteria
Happy Path: One-Tap Usernameless Passkey Sign-In
Given a returning user has a discoverable passkey for the RP on the current device And the user is on the sign-in screen When the user taps "Sign in" and completes biometric verification Then navigator.credentials.get is invoked without allowCredentials And the platform selector performs account discovery (no username prompt shown) And the backend verifies rpId, origin, challenge, clientDataJSON.type, signature, authenticatorData flags (including user verification), and advances signCount when present And a session and refresh token are issued And the app navigates to the home screen
WebAuthn Parameters: Usernameless Discoverable Credential Flow
- navigator.credentials.get uses publicKey with allowCredentials omitted or empty - publicKey.userVerification is set to "required" - publicKey.rpId matches the effective RP domain - The UI does not request or display a username prior to invoking the platform selector - The call relies on discoverable credentials; account discovery is handled by the platform selector - Errors from get() are captured and mapped to actionable UI states
Performance SLA: End-to-End Auth <= 2 Seconds
- Instrument client and server to measure total time from tap to authenticated home screen - 95th percentile of successful sign-ins completes in <= 2.0 seconds on supported devices and typical network conditions - Metrics are logged per sign-in attempt and exposed to monitoring with P50/P95 - Attempts exceeding the threshold are tagged for analysis in telemetry
Multiple Passkeys per User: Correct Account Resolution
- A user with multiple registered passkeys (cross-platform or platform) can sign in with any of them - Server maps the credentialId to the same userId regardless of which registered passkey is used - No duplicate accounts are created during authentication - Successful assertions record credential metadata (credentialId hash/AAGUID) in audit telemetry - Attempts using a deleted/unrecognized credentialId are rejected with a specific error and recovery guidance
Signature Counter and Cloned Authenticator Handling
- On successful assertion, if signCount > stored for the credential, persist the new signCount - If signCount < stored for the credential, reject the authentication, log a security event (signcount_regression), and present re-enrollment guidance - If signCount is 0 or absent, allow authentication but mark the credential as no-counter in telemetry for risk review - All outcomes are covered by automated tests simulating increasing, equal, and regressing counters
Fallback and Error UX for Passkey Failures
- Passkey is the default sign-in path for returning users - If get() returns NotAllowedError, NotSupportedError, or no discoverable credentials are found, show inline error with actions: "Try again" and "Use another method" - No automatic fallback occurs without explicit user action - Retry path re-invokes passkey without page reload and preserves app state - Error events are captured with reason codes for analytics
Session Creation, Refresh, and Logout Invalidation
- After successful verification, server issues access and refresh tokens and associates them with device telemetry - Subsequent protected API calls succeed without additional auth prompts while the session is valid - Access tokens can be refreshed without user interaction while the refresh token is valid - On logout, tokens are invalidated server-side; subsequent protected requests return unauthorized until re-authentication - Replayed assertions with stale challenges are rejected
Cross-Device Account Linking
"As a user who got a new phone, I want to link my StreakShare account to it quickly so that I can keep my streaks without resetting my access."
Description

Provide a seamless way to access the same account on additional devices by leveraging synced passkeys where supported and offering a guided fallback when not. When users add a new device, first attempt platform-synced passkeys (iCloud Keychain, Google Password Manager) for instant recognition. If unavailable, provide a secure bootstrap flow: scan a QR code to approve from a signed-in device (caBLE/passkey share or app-to-app link) or use a verified magic link/OTP to confirm account ownership, then prompt immediate passkey enrollment on the new device. Ensure the linking flow binds devices to the same user profile, prevents account duplication, and gives clear, minimal-step instructions. All flows must be resistant to phishing by enforcing origin/app package checks and must avoid exposing email/phone publicly during discovery.

Acceptance Criteria
Instant Link via Synced Passkey
Given a user has an existing account with a passkey synced via iCloud Keychain or Google Password Manager When they open StreakShare on a new device and choose Link Existing Account Then the OS-native passkey sheet is invoked within 1 second And selecting the matching passkey signs them in and links the device within 3 seconds And the user's profile, streaks, and rooms match the source device exactly (0 discrepancies) And no new account record is created And an analytics event link_synced_passkey_success is recorded without PII Security: The passkey assertion is verified against the expected RP ID/package signature; mismatched origin/package attempts are rejected without revealing account existence
QR Approve from Signed-in Device
Given the user is signed in on at least one other device When they select Approve via QR on the new device Then the new device displays a QR containing a signed nonce scoped to RP ID and app package with 5-minute expiry And scanning the QR on the signed-in device opens an in-app approval modal via deep link And after biometric confirmation, the new device is signed in and linked within 5 seconds And the new device is prompted to enroll a device-bound passkey immediately; if the platform cannot store passkeys, the user is informed and re-prompted within 7 days And no full email or phone is displayed on either device prior to confirmation (masked only) Security: Replay, cross-origin, or expired QR attempts are rejected and rate-limited to 5 attempts per 10 minutes
Magic Link or OTP Fallback
Given no synced passkey is available and no signed-in device is present When the user selects Verify another way Then the app shows contact options in masked form only (e.g., j***@d***.com, ***-***-1234) And sends a single-use magic link and a 6-digit OTP valid for 10 minutes And opening the magic link on the new device or entering the OTP signs the user in and links the device And the app immediately prompts passkey enrollment on the new device; skipping is allowed but re-prompted on next launch Security: The magic link is bound to the initiating device session, expires after first use, and 5 failed OTP attempts within 15 minutes trigger a 15-minute cooldown without revealing which contact is valid
Prevent Account Duplication and Data Integrity
When linking completes via any supported method Then the device is associated with the existing user ID and no additional account record is created (atomic upsert) And if a local soft account exists, data is merged without loss And post-link, streak counts and room memberships match the source device; any mismatch triggers rollback and an error with a retry option And concurrent link attempts from multiple devices result in one successful bind and idempotent responses for others
Phishing Resistance and Origin Enforcement
All linking methods enforce TLS and verify RP ID streakshare.app (web) or the expected Android/iOS package identifiers WebView or insecure context initiations are blocked with a generic error that reveals no account existence All QR, magic link, and OTP challenges are signed, session-bound, and include expirations; unsigned or expired challenges are rejected No API or UI in the flow reveals full email or phone; discovery endpoints return generic responses regardless of contact validity
Performance, Steps, and UX Defaults
Synced passkey path completes in <= 2 taps and <= 5 seconds median on reference network (4G) QR approval path completes in <= 4 taps across both devices and <= 10 seconds median Magic link/OTP path completes in <= 6 taps and <= 2 minutes at the 90th percentile On the entry screen, the highest-trust available method is auto-highlighted (synced passkey > QR > magic/OTP) All screens are usable without scrolling on a 360x640 dp reference device and support system dark mode
Error Handling and Recovery
Network loss during linking preserves state and offers a Resume option for 24 hours; retries are idempotent and do not create duplicate devices or accounts Incorrect OTP, expired magic link, or denied biometric displays a generic, actionable error and offers the next-best fallback method After three consecutive failures of any single method, the flow suggests an alternative method and shows support options without exposing PII All failures are logged with anonymized diagnostics; no stack traces or secrets are shown to the user
Recovery and Fallback Authentication
"As a user who can’t use my biometric, I want a fast recovery option so that I can regain access and re-enable one-tap sign-in without starting over."
Description

Offer secure, low-friction recovery when biometrics or passkeys are unavailable by providing time-bound magic links or one-time codes and backup codes for last-resort access. Recovery flows should verify possession of a registered email/phone, enforce rate limits and risk checks, and on success immediately prompt the user to re-enroll a passkey to restore one-tap access. Include device loss/change scenarios and allow revocation of old device credentials from the new session. Provide clear guidance in failure states, and avoid password creation to preserve the passwordless model. Ensure codes/links have short TTLs, single use, regional SMS/Email deliverability checks, and audit logs for administrative review.

Acceptance Criteria
Email Magic Link Recovery
Given a user with a verified email selects "Email me a magic link" for account recovery When the user requests a magic link Then the system sends a single-use recovery link to the registered email within 5 seconds And the link has a TTL of 10 minutes from issuance And only the most recently issued, unconsumed link remains valid; all prior links are invalidated And clicking a valid, unconsumed link creates a new authenticated session and marks the token as consumed And if the email provider reports a hard bounce or regional block, no link is issued and the UI offers phone/SMS fallback And after successful recovery the user is immediately prompted to re-enroll a passkey before accessing the home screen And an audit entry records issuance, delivery status, and consumption outcomes
SMS One-Time Code Recovery with Deliverability and Limits
Given a user with a verified phone selects "Text me a code" for account recovery When the system checks SMS deliverability for the user’s country code and carrier Then if deliverability is acceptable, a 6-digit numeric OTP is sent within 5 seconds And if deliverability is degraded, the system does not send SMS and offers an email magic link fallback And the OTP has a TTL of 5 minutes and allows a maximum of 5 verification attempts And sending is rate-limited to 3 OTPs per 30 minutes per account and IP and 10 per 24 hours And upon correct OTP entry a new authenticated session is created and the OTP is invalidated And after successful recovery the user is immediately prompted to re-enroll a passkey And failures (expired, attempts exceeded, rate-limited) return clear, non-enumerating messages
Backup Codes Generation and Use
Given a signed-in user navigates to Security > Backup Codes When the user generates backup codes Then the system creates 10 single-use alphanumeric codes, each 10 characters long And the codes are displayed once with copy, download, and print options and are never shown again And regenerating codes immediately invalidates all previously issued backup codes And using a valid backup code during recovery creates a session and consumes that code And after successful recovery via backup code the user is prompted to re-enroll a passkey And all generation, regeneration, and consumption events are audited
Device Loss Recovery and Credential Revocation
Given a user completes recovery and indicates "I lost or changed my device" When the new session is established Then the user is shown a list of registered passkey credentials with device nickname and last-used timestamp And the user can revoke one or more credentials, taking effect within 10 seconds And subsequent sign-in attempts with a revoked credential are rejected with error code AUTH_CREDENTIAL_REVOKED And revocations are confirmed to the user and recorded in the audit log And the user is guided to register a new passkey before full access resumes
Risk Checks and Adaptive Requirements
Given any recovery attempt is initiated When the request originates from a high-risk context (risk score >= threshold, new device + new country, or TOR/restricted ASN) Then the system requires possession of both channels (email magic link confirmation and SMS OTP) before creating a session And otherwise permits single-channel recovery And per-account and per-IP/global rate limits are enforced, returning standardized errors RATE_LIMITED or RISK_BLOCKED with retry-after where applicable And a 15-minute lockout is applied after limit exceedance And all risk evaluations and outcomes are recorded with reason codes
Failure Guidance Without Passwords
Given a recovery attempt fails due to expired/used token, wrong code, rate limit, or deliverability failure When the error state is shown to the user Then the UI displays a clear message, next steps, and a support link without revealing account existence And the UI provides an alternative available channel (email or SMS) when one channel fails And the UI never offers password creation or password setup at any point in recovery And all failures include a non-PII request identifier for support tracing
Audit Logging and Administrative Review
Given any recovery-related event occurs (send, verify, consume, revoke, re-enroll) When the event is processed Then an immutable audit record is written with: userId or pseudonymousId, eventType, channel, timestamp (ms), IP, userAgent, geo-country, requestId, outcome, reasonCode, and credentialId where applicable And audit records are queryable by authorized admins within 60 seconds of event creation And logs are stored append-only with a daily hash chain for tamper evidence And admins can export logs by date range and eventType; access to PII is role-restricted and itself audited
Session Management and Trust Refresh
"As a daily user, I want my session to stay active on my device so that I can check in instantly without repeated prompts."
Description

Implement session lifecycles optimized for frequent, quick check-ins: short-lived access tokens with silent refresh, persistent device-bound sessions, and lightweight re-auth prompts only for sensitive actions. Tie sessions to passkey-authenticated devices and rotate refresh tokens on use to reduce hijack risk. Establish inactivity and absolute expiry policies that balance security with convenience, ensuring daily users rarely see prompts while lapsed users can return smoothly with one tap. Provide server-side session revocation when credentials are removed or risk signals trigger, and synchronize session state across devices to prevent confusion. Maintain sub-50 ms server-side token checks to keep check-in flows instant.

Acceptance Criteria
Silent Access Token Refresh During Check-In
Given a signed-in user on a passkey-enrolled device with an access token TTL of 10 minutes and a valid device-bound refresh token When the user taps "Check In" and the access token is expired or will expire within 60 seconds Then the client performs a silent refresh, the server validates and rotates the refresh token, and a new access token and refresh token are issued And the check-in request proceeds without any authentication UI And the server-side token check and refresh path adds <= 50 ms p95 and <= 75 ms p99 latency as measured by APM traces And no biometric prompt is shown during the check-in flow
Device-Bound Sessions with Passkey Enforcement
Given a refresh token issued to Device A and bound to Device A's device identifier and passkey When a refresh attempt using that token originates from Device B Then the server responds 401 invalid_grant, marks the token as compromised, and revokes the session associated with that token And Device A's next API call is forced to refresh with a new token pair And no cross-device refresh succeeds without a valid passkey assertion from the originating device
Sensitive Actions Require Lightweight Re-Auth
Given an active session When the user initiates a sensitive action (change email, change payout method, view/export personal data, delete account, manage passkeys/devices) Then an OS-native biometric prompt is required before the action API executes And upon successful biometric assertion within the last 5 minutes, subsequent sensitive actions in that window do not prompt again And on biometric failure or cancellation, the action is blocked and no server-side state changes occur And non-sensitive actions (e.g., daily check-in, viewing rooms, reacting) never trigger a re-auth prompt
Inactivity Soft Timeout and Absolute Expiry
Given inactivity thresholds of 7-day soft timeout and 30-day absolute expiry When a user returns after <= 7 days of inactivity Then no re-auth prompt is shown and the session silently refreshes as needed When a user returns after > 7 days and < 30 days of inactivity Then a single OS biometric prompt re-establishes the session (one tap) and new tokens are issued When a user returns after >= 30 days of inactivity or the absolute expiry is reached Then a one-tap biometric sign-in is required and all previous refresh tokens are invalid; new device-bound tokens are issued
Refresh Token Rotation and Replay Protection
Given a valid refresh token When it is used to obtain a new access token Then the server rotates the refresh token and invalidates the prior one atomically And any subsequent attempt to reuse the prior refresh token returns 401 and flags the session for risk review And concurrent refresh attempts with the same prior token result in exactly one success and all others fail without issuing multiple token pairs
Revocation Propagation and Cross-Device Sync
Given a user account with active sessions across multiple devices When the user removes a passkey, triggers "Sign out of all devices", or a risk signal exceeds the block threshold Then the server revokes all active access and refresh tokens within 60 seconds And all devices reflect the signed-out state on the next API call or on app foreground within 60 seconds And any API call using revoked tokens receives 401 unauthorized with a machine-actionable error code And the session list endpoint reflects revocation across devices within 2 seconds of completion
Token Validation Performance Budget
Given normal production traffic up to 200 requests per second per region When validating access tokens server-side (without network latency) Then p95 validation time is <= 50 ms and p99 <= 75 ms over a rolling 5-minute window And 99.99% of token validation attempts complete without internal retries or timeouts And real-time check-in flows that include one token validation complete without server-side token validation errors for 99.95% of requests
Enrollment Funnel Analytics
"As a product manager, I want clear analytics on the one-tap flow so that I can identify friction and improve completion rates."
Description

Instrument the one-tap enrollment and sign-in flows to quantify drop-off and performance, enabling rapid iteration. Track key events (seen, started, OS prompt shown, success, error types, retries), timings (time-to-prompt, prompt-to-complete), capability detection outcomes, and recovery usage. Define a privacy-respecting schema with no sensitive biometric data, pseudonymous user identifiers, and regional data residency adherence. Provide dashboards that segment by platform, app version, locale, and entry point (onboarding vs. next sign-in) to surface friction hotspots. Emit operational alerts for spikes in failures or latency regressions and maintain experiment flags for A/B testing prompt copy or placement.

Acceptance Criteria
Track One-Tap Enrollment Funnel Events
Given a user enters the One-Tap Enroll flow, When key funnel milestones occur, Then the app emits the following events exactly once per occurrence with required fields and idempotency: - enrollment_view_seen - enrollment_started - os_prompt_shown - enrollment_success - enrollment_error - enrollment_retry And all events include fields: event_id (UUIDv4), user_pseudo_id, platform (iOS|Android|Web), app_version (semver), locale (BCP47), entry_point (onboarding|next_sign_in), timestamp_ms (UTC epoch), session_id, and experiment labels if applicable. And enrollment_error additionally includes: error_code (from controlled enum), os_error_domain (from controlled enum), retryable (boolean). And enrollment_retry includes: retry_count (integer >=1) and last_error_code. And duplicates received within 24h are de-duplicated by event_id server-side. And events are queued offline and delivered within 5 minutes of connectivity restoration.
Measure Enrollment Timing Metrics
Given an enrollment flow starts, When os_prompt_shown and either enrollment_success or enrollment_error are emitted, Then time_to_prompt_ms = os_prompt_shown.timestamp_ms - enrollment_started.timestamp_ms is recorded. And prompt_to_complete_ms = completion_event.timestamp_ms - os_prompt_shown.timestamp_ms is recorded. And both latency metrics are attached to the terminal event (success or error) and fall back to null if an intermediate event is missing. And timing is measured with a monotonic clock where available and converted to ms. And client-side validation ensures time_to_prompt_ms and prompt_to_complete_ms are in [0, 120000] ms; out-of-range values are dropped with a local log. And server dashboards expose p50/p90/p95 for both metrics by day and by segment.
Detect Device Passkey Capability and Outcomes
Given the enroll screen initializes, When capability detection runs, Then an enrollment_capability event is emitted once per session with fields: supported (boolean), reason_code (none|os_version_min|device_policy|no_biometric|no_secure_hardware|transport_unavailable), platform_authenticator (boolean), passkey_provider (os_builtin|password_manager|unknown). And if supported=false, Then the next step taken emits enrollment_recovery_used with fields: recovery_path (password_fallback|magic_link|skip) and completed (boolean). And no biometric samples, templates, or match scores are recorded; only capability booleans/enums are logged. And capability detection latency is captured as detection_ms and validated to be <= 500 ms on p95 in dashboards.
Privacy-Respecting Analytics Schema and Residency
Given analytics payloads are generated, When events are validated against the schema, Then no fields containing personal identifiers (name, email, phone, IP, precise location) or biometric data are permitted. And user_pseudo_id is a pseudonymous, randomly generated identifier not derived from PII and stable per install/account as specified. And events are routed and stored in the user account’s configured data region (e.g., EU, US) and do not egress that region. And users who opt out of analytics do not emit any enrollment analytics events. And data retention for these events is capped at 13 months with documented deletion on account erasure. And a schema lint fails builds if prohibited fields (matching a maintained denylist) are added.
Enrollment Funnel Dashboard with Segmentation
Given events are flowing, When a product analyst opens the Enrollment Funnel dashboard, Then the dashboard shows a stepwise funnel (viewed → started → OS prompt → success) with counts and conversion rates. And filters are available for platform, app_version, locale, entry_point, and date range, and can be combined. And timing widgets display p50/p90/p95 for time_to_prompt_ms and prompt_to_complete_ms per selected segment. And error breakdown shows top error_code and os_error_domain with percentages. And dashboard refresh latency is <= 15 minutes for streaming data and clearly labeled. And dashboard loads in < 5 seconds at p90 for commonly used segments.
Operational Alerts for Failures and Latency Regressions
Given baseline metrics exist for the last 7 days, When enrollment_error rate exceeds baseline by >= 2x and >= 3 percentage points for 10 consecutive minutes with N>=100 attempts, Then a High Failure Rate alert is triggered to on-call via Slack and email with a link to the dashboard and runbook. And when p95 prompt_to_complete_ms increases by >= 25% over baseline for 15 minutes with N>=100 attempts, Then a Latency Regression alert is triggered with the same routing. And alerts auto-resolve when metrics return within thresholds for 15 minutes. And alerts are deduplicated per platform and app_version to avoid flooding. And a synthetic monitor runs hourly to validate event ingestion end-to-end; ingestion failures > 5% trigger an Ingestion Health alert.
Experiment Flags for Prompt Copy/Placement
Given an A/B test is configured, When users enter the enroll flow, Then they are bucketed by user_pseudo_id with stable assignment and target split (e.g., 50/50) per platform. And an experiment_exposure event with fields {experiment_name, variant} is emitted before enrollment_started. And all subsequent funnel events include experiment_name and variant fields. And guardrail metrics (error rate, p95 latencies) are computed per variant in the dashboard. And a kill switch allows disabling any variant within 5 minutes, verified by exposure counts dropping to zero. And bucketing bias is < 1% absolute difference across key segments (platform, locale) as verified by a balance check widget.
Security Controls and Attestation Policy
"As a security-conscious user, I want strong protections around one-tap sign-in so that my account and streaks remain safe from fraud."
Description

Harden the passkey flows with origin and RP ID enforcement, replay-resistant challenges, strict TLS, and configurable attestation policy. Default to privacy-preserving attestation (none) while allowing policy-based acceptance or logging of certain authenticator AAGUIDs for fraud analysis. Implement rate limiting, bot/automation defenses, IP/device fingerprint risk scoring, and step-up verification on anomalous attempts. Validate signature counters to detect cloned credentials and provide admin tools to revoke credentials or sessions. Store credential metadata securely, minimize PII, and align with OWASP ASVS, platform guidelines, and GDPR. Maintain comprehensive audit logs for registration, authentication, recovery, and revocation events with tamper-evident storage.

Acceptance Criteria
Origin, RP ID, and TLS Enforcement on Passkey Flows
- Given a WebAuthn registration or authentication request, When the client origin is not in the allowed origin list for the current environment, Then the server rejects with HTTP 400 reason "invalid_origin" and writes an audit event containing origin and rpId. - Given WebAuthn processing, When the rpId does not match the configured rpId "streakshare.com" (including only permitted subdomains), Then the server rejects with HTTP 400 reason "invalid_rpId" and writes an audit event. - Given any passkey endpoint, When accessed over non-TLS, Then the connection is refused and HTTP 301 redirects to HTTPS in non-API contexts; API endpoints over HTTP return HTTP 400 reason "insecure_transport". - Given production traffic, When responses are served from auth endpoints, Then the Strict-Transport-Security header is present with max-age >= 31536000, includeSubDomains, preload. - Given a cross-origin iframe invocation, When the origin is not explicitly allowed by policy, Then WebAuthn operations are blocked and an audit event is recorded with reason "disallowed_cross_origin".
Replay-Resistant Challenge Lifecycle
- Given a registration or authentication initiation, When a challenge is generated, Then it is created via a CSPRNG with >=128 bits of entropy and stored server-side with a TTL of 120 seconds. - Given a submitted WebAuthn response, When the challenge is expired, missing, or does not match the last issued value for that session, Then the request is rejected with HTTP 400 reason "invalid_or_expired_challenge" and the event is audited. - Given a previously used challenge, When it is presented again, Then the request is rejected with HTTP 400 reason "challenge_replay" and the event is audited with the related requestId. - Given normal operation, When multiple concurrent requests are made, Then each challenge is unique per request and single-use, and server verifies nonce uniqueness before marking consumed.
Configurable Attestation Policy with Privacy-Preserving Default
- Given default configuration, When creating registration options, Then attestation conveyance is set to "none" and the server does not persist attestation certificates or identifying device info beyond AAGUID if policy requires logging. - Given policy mode "log-only", When an authenticator provides attestation, Then registration succeeds, the AAGUID and attestation format are recorded in audit logs, and no attestation certificate chain is stored. - Given policy mode "enforce-allowlist", When the AAGUID is not in the allowlist, Then registration is rejected with HTTP 403 reason "attestation_denied" and the attempt is audited; when the AAGUID is allowed, registration succeeds and AAGUID is stored with the credential. - Given an admin with proper role, When updating attestation policy via API/UI, Then changes require authenticated requests, are versioned, take effect within 1 minute, and the change event is audited with old/new values.
Adaptive Abuse Controls: Rate Limiting and Bot Mitigation
- Given registration attempts, When more than 3 creates per IP or device fingerprint occur within 10 minutes, Then subsequent attempts receive HTTP 429 with a Retry-After header and the throttle event is logged. - Given authentication attempts, When more than 10 attempts per account within 5 minutes or more than 20 per IP within 1 minute occur, Then subsequent attempts receive HTTP 429 with a Retry-After header and the throttle event is logged. - Given elevated abuse signals, When thresholds are crossed, Then a bot challenge is presented (e.g., proof-of-work or CAPTCHA) and only upon success is the flow resumed; failures return HTTP 403 reason "bot_verification_failed". - Given operational monitoring, When throttling occurs, Then metrics (rate_limit.count, captcha.presented, captcha.failed) are emitted with tags (endpoint, env) for observability.
Risk Scoring and Step-Up Verification on Anomalous Attempts
- Given an authentication attempt, When the risk score (IP reputation, ASN, TOR/VPN, geo-velocity, device fingerprint mismatch) exceeds the high-risk threshold, Then step-up verification (e.g., email OTP) is required before completing authentication. - Given a medium-risk score, When user has an active trusted device cookie, Then allow with additional friction (e.g., re-prompt biometric) and log reason codes; otherwise require step-up. - Given a failed step-up, When the user cannot complete within 5 minutes or 3 attempts, Then the authentication is denied with HTTP 401 reason "step_up_failed" and audited with risk factors. - Given successful step-up, When completed within the window, Then the session is established and flagged with risk.level and step_up.method in the session metadata.
Signature Counter Validation and Clone Detection Response
- Given a successful assertion, When the authenticator signCount is > 0 historically and the current signCount <= the stored signCount, Then mark the credential with status "suspected_clone", require step-up for this session, and write an audit event reason "counter_regression". - Given repeated counter regression, When a second regression occurs within 30 days for the same credential, Then revoke the credential, invalidate active sessions tied to it, notify the user via email, and audit the revocation. - Given authenticators that report signCount = 0 (no counters) or passkeys known to be non-incrementing, When validating, Then do not trigger clone detection solely on signCount and instead rely on risk scoring. - Given an admin with appropriate role, When revoking or reinstating a credential, Then the action succeeds via API/UI, takes effect immediately, and is fully audited (who, when, which credentialId, reason).
Secure Credential Metadata Storage and PII Minimization
- Given credential storage, When persisting credentialId, publicKey, rpId, userHandle, AAGUID, and signCount, Then data is encrypted at rest using AES-256-GCM with keys managed in a FIPS-validated KMS and rotated at least every 90 days. - Given application roles, When accessing credential metadata, Then only the auth service role with least-privilege can read/write, and access is denied to other services; all accesses are audited. - Given data minimization, When storing registration/authentication records, Then no biometric data or raw attestation certificates are stored by default, and IP/device fingerprints used for risk are stored separately with a TTL of 30 days. - Given compliance, When exporting or deleting user data (DSAR), Then credential metadata can be exported without secret keys and deleted within 30 days, while security logs are retained/anonymized per policy and GDPR lawful basis.

Stay Signed

Silent, passkey-anchored session refresh keeps you authenticated so Auto-Room Links, lockscreen check-ins, and watch actions just work. No surprise logouts, fewer broken flows, and a smoother path to rescuing streaks. Biometric prompts appear only for sensitive changes.

Requirements

Passkey-Anchored Login & Session Binding
"As a returning user, I want my session anchored to my device passkey so that I stay signed in without passwords and without surprise logouts."
Description

Implement passkey-based authentication using WebAuthn/FIDO2 platform authenticators to bind a user’s session to a trusted device credential. On successful registration/login, mint short-lived access tokens and a device-bound refresh token stored in secure OS keystores (iOS Keychain, Android Keystore, Web Credential Management). Tie refresh eligibility to the passkey credential ID and device fingerprint to mitigate token theft. Provide graceful fallback flows for non-passkey environments while keeping the primary path passwordless and phishing-resistant. This foundation ensures users remain signed in across app restarts and updates without surprise logouts, aligning with StreakShare’s low-friction habit flows.

Acceptance Criteria
Passkey Registration on Supported Device
Given the device supports a WebAuthn platform authenticator and the user initiates account creation or adds a new device When the user selects "Create passkey" Then a WebAuthn registration ceremony starts with the RP ID matching the StreakShare domain And a platform credential is created (public key algorithm ES256 or equivalent) and its credential ID is stored server-side And the server mints an access token with TTL ≤ 15 minutes and a device-bound refresh token And the refresh token is cryptographically bound to the passkey credential ID and device fingerprint And the refresh token is stored only in the secure OS keystore (iOS Keychain, Android Keystore, Web Credential Management API) And no password is requested at any step
Passkey Login on Returning Device
Given a passkey credential exists on the device and the user is logged out When the user selects "Sign in with passkey" and completes the WebAuthn authentication ceremony Then the server issues a new short-lived access token and rotates the device-bound refresh token And the tokens are written/updated in the secure OS keystore And the user is navigated to the home screen without additional prompts for non-sensitive actions And the end-to-end sign-in completes within 3 seconds under p50 conditions
Silent Session Refresh and Binding Enforcement
Given the access token is expired and a valid device-bound refresh token exists in the OS keystore When the app requests a background token refresh Then the server validates the refresh token binding (credential ID and device fingerprint) against the stored record And on success, a new access token is issued and the refresh token is rotated; the previous refresh token becomes invalid immediately after successful rotation And no user prompt is shown and active flows (Auto-Room Links, lockscreen check-ins, watch actions) continue without interruption And if the credential ID or device fingerprint do not match, the server returns 401 with error code "refresh_binding_mismatch" and no new tokens are issued
Persisted Sign-In Across Restarts and Updates
Given a user has a valid device-bound refresh token in the OS keystore When the app is restarted or updated to a new version Then the user remains signed in without manual re-authentication And if the access token is expired, a silent refresh is attempted on first foreground within 1 second of launch And if the refresh token exceeds max lifetime (e.g., 90 days since last use) or the keystore reports key invalidation, the user is prompted to re-authenticate with a passkey And no unexpected logout occurs during normal usage within the refresh token lifetime
Biometric Prompt for Sensitive Changes
Given the user is signed in When the user attempts a sensitive action (email/phone change, add/remove passkeys, export data, delete account) Then a fresh WebAuthn assertion with a biometric prompt is required within the last 2 minutes And on success, the action proceeds; on failure or timeout, the action is blocked with error "reauth_required" And routine actions (check-ins, reactions, opening rooms) do not trigger a biometric prompt
Non-Passkey Environment Fallback
Given the device/browser does not support platform passkeys or the user declines passkey use When the user selects the fallback sign-in Then a passwordless phishing-resistant option (e.g., magic link) constrained to the StreakShare domain/app is provided And upon successful fallback authentication, the server mints the same short-lived access token and a device-bound refresh token stored in the OS keystore And the user is prompted post-login to enroll a passkey when capability is detected And fallback sessions enforce the same binding rules; refresh from a different device fails with 401 "refresh_binding_mismatch"
Device and Credential Revocation
Given a user initiates revocation for a device or removes a passkey credential When the revocation is confirmed Then the server invalidates all refresh tokens bound to that credential/device within 60 seconds And subsequent refresh attempts from the revoked device return 401 "device_revoked" And other devices remain signed in unless explicitly revoked And the account activity view lists the revoked device with a revocation timestamp
Silent Session Refresh & Rotation
"As a mobile user, I want my session to refresh silently in the background so that check-ins and actions keep working without interruptions."
Description

Create a background refresh mechanism that renews access tokens before expiry without user prompts. Use rotating refresh tokens, sliding session windows, and strict audience/scope checks. Implement client interceptors to trigger refresh on 401/near-expiry, with exponential backoff and offline handling (queue and retry on connectivity). On mobile, leverage background fetch/app resume hooks; on web, use service workers while the app is active. Ensure refresh endpoints validate device binding and enforce rate limits. Log telemetry for refresh success/failure to monitor reliability and catch regressions. The outcome is uninterrupted authentication continuity for everyday actions and check-ins.

Acceptance Criteria
Proactive Token Refresh Before Expiry
Given an authenticated user with an access token expiring in 5 minutes or less and a valid refresh token When the app is foregrounded, a background task runs, or a service worker heartbeat occurs Then the client requests a token refresh without prompting the user And the server issues a new access token and a rotated refresh token with an updated sliding window And the client replaces stored tokens atomically and uses the new access token for subsequent calls And the client verifies the new token audience and scopes match expected values before use; otherwise it discards the token and treats refresh as failed And in-flight user requests complete successfully without user-visible 401 errors
401 Interceptor With Exponential Backoff and Retry
Given any API call returns 401 due to an expired or invalid access token When the client interceptor handles the response Then it initiates a token refresh and retries the original request upon refresh success And it applies exponential backoff delays of 200 ms, 400 ms, and 800 ms capped at 2 s, with up to 3 total refresh attempts And on 403 or invalid_grant from the refresh endpoint, it stops retrying and surfaces a non-blocking re-auth required state without logging the user out And each original request is retried at most once after a successful refresh
Token Rotation and Single-Flight Concurrency Safety
Given multiple concurrent requests detect an expiring token or receive 401 responses When a refresh is needed Then the client performs a single in-flight refresh; other requests await its result And upon refresh success, a new refresh token is stored and older refresh tokens are not reused And if a stale refresh token is used and the server returns a token family error, the client reattempts once with the latest token and records the incident And no duplicate refresh calls are made during a 2-second coalescing window
Offline Queue and Connectivity Retry
Given the device is offline when a refresh is due or a 401 is received When offline is detected Then the client queues the refresh attempt and any pending requests without prompting the user And upon connectivity restoration, the client triggers refresh within 2 seconds and replays queued requests in original order And mutating requests include idempotency keys to prevent duplicate side effects on replay And if the sliding session window has expired before connectivity returns, the client presents a non-blocking re-auth prompt on next foreground without auto-logout
Platform Background Refresh Hooks (Mobile and Web)
Given iOS or Android with background fetch enabled and token TTL is under 30 minutes When the OS grants a background fetch window Then the client attempts a refresh within that window without user prompts And upon app resume, if token TTL is under 15 minutes or expired, the client refreshes before protected actions proceed Given a web client with an active page and a registered service worker When token TTL is under 5 minutes or a fetch is intercepted with 401 Then the service worker performs a single-flight refresh, updates clients via messaging, and attaches the latest access token to subsequent fetches And the service worker ceases refresh activity when no client pages are active
Refresh Endpoint Device Binding and Rate Limiting
Given a refresh request is submitted When the server validates device binding using a device identifier and signed key proof Then a mismatch results in 403, the refresh token is revoked, and the client enters a re-auth required state And successful refreshes are limited to a maximum of 4 per minute per user-device; excess attempts return 429 And upon receiving 429, the client backs off for at least 60 seconds before retrying and does not issue additional refresh attempts during the backoff window And the client validates that new tokens include the expected audience and scopes before accepting them
Refresh Telemetry and SLOs
Given any refresh attempt completes or fails When telemetry is recorded Then client and server emit structured events for refresh_success, refresh_fail, refresh_retry, offline_queue, and rate_limit_hit including timestamp, hashed device_id, latency_ms, attempt_count, and result_code And daily metrics report a refresh success rate of at least 99% over the past 24 hours with alerts when below 98.5% And the 95th percentile refresh latency is at or below 800 ms over a 30-minute window with alerts when exceeded And at least 95% of requests retried after a successful refresh complete with 2xx within 2 seconds
Auth Continuity for Auto-Room Links
"As a user tapping an Auto-Room link, I want to land in the room instantly while staying signed in so that I can check in with no friction."
Description

Ensure deep links (iOS Universal Links, Android App Links, web) open directly into rooms with valid auth. On link open, preflight token status; if near expiry, run a silent refresh before navigation. If refresh fails, fall back to a one-tap passkey assertion to recover seamlessly. Preserve link context (room ID, intent) across auth steps and prevent duplicate navigations. Add analytics for link->room time, refresh attempts, and failures to optimize. This guarantees Auto-Room Links consistently land users in the right place without breaking the flow.

Acceptance Criteria
Direct Open with Valid Session
Given a deep link to a room (iOS/Android/Web) and an access token with >10 minutes remaining When the user opens the link Then the app navigates directly to the target room view without displaying any authentication UI And navigation completes within 2000 ms (p95) under controlled network latency ≤100 ms And an analytics event "deeplink_room_open" is emitted with auth_path="valid" and a non-null link_to_room_ms
Silent Refresh on Near-Expiry Session
Given a deep link to a room and an access token expiring in ≤10 minutes with a valid refresh token When the link is opened Then the app performs a silent token refresh before navigating And no biometric/passkey prompt is displayed And a new access token is stored and used for the room request And navigation completes within 2500 ms (p95) under controlled network latency ≤100 ms And analytics include refresh_attempts=1, refresh_result="success", auth_path="silent_refresh"
Passkey Fallback on Refresh Failure
Given a deep link to a room and the token preflight determines that refresh is required but the refresh attempt fails (e.g., invalid/expired refresh token) When the link is opened Then a one-tap passkey/WebAuthn assertion prompt is shown And upon successful assertion, the app navigates to the target room and executes the intended action And if the user cancels or the assertion fails, the link context is preserved for 10 minutes with a retry CTA, and no blank/error room is shown And analytics record auth_path="passkey", refresh_attempts≥1, and failure_reason when applicable
Context Preservation Across Auth Steps
Given a deep link containing room_id and intent (e.g., join, check_in, view) When any authentication step occurs (silent refresh or passkey) Then room_id and intent are preserved end-to-end and applied after authentication And the intended action executes exactly once And query parameters unrelated to auth are not lost or mutated
Duplicate Navigation Prevention
Given the same deep link is activated multiple times within 5 seconds or the OS delivers the link twice When authentication and navigation proceed Then only one room view is presented/pushed And only one join/open network request is executed And only one "deeplink_room_open" analytics event is emitted (idempotent handling)
Telemetry Completeness and Schema
Given any deep link attempt When the flow completes (success or failure) Then the following events are emitted with the listed required properties: - deeplink_received: platform, link_source, room_id_hash, intent, ts - deeplink_auth_preflight: token_status, ts - deeplink_auth_refresh (when applicable): attempt, result, error_code (on failure), ts - deeplink_passkey_prompt (when applicable): shown=true, result, ts - deeplink_room_open (on success): auth_path, link_to_room_ms, refresh_attempts, ts - deeplink_failure (on failure): stage, error_code, ts And 99% of successful opens contain a non-null link_to_room_ms And all events pass schema validation in the analytics pipeline
Cross-Platform Deep Link Handling
Given iOS Universal Links and Android App Links are properly associated and the app is installed When a user taps an Auto-Room Link from Messages, Email, or Browser Then the native app opens directly and follows the Auth Continuity flow to the target room And if the app is not installed, the link falls back to web, performs the same preflight/refresh/passkey logic, and lands in the correct room after web sign-in And the association files (apple-app-site-association, assetlinks.json) are verified and pass OS validators
Lockscreen & Watch Check-in Authorization
"As a user using a lockscreen widget or watch app, I want check-ins to work while staying authenticated so that I can maintain my streak with one tap."
Description

Enable lockscreen widgets, quick settings tiles, and watch apps to perform one-tap check-ins using scoped, short-lived tokens or signed commands derived from the primary session. Store extension credentials in secure enclaves and restrict capabilities to check-in actions only. If a token is invalid or expired, attempt a silent refresh via the paired app; if risk is detected, escalate to a passkey prompt on the phone. Support offline queuing with later sync. This preserves the ‘just works’ experience on peripherals without compromising account security.

Acceptance Criteria
One-Tap Lockscreen/Watch Check-in with Valid Token
Given the user has an active primary session and has enabled a lockscreen widget, quick settings tile, or watch app And a scoped check-in token derived from the primary session is valid (TTL ≤ 5 minutes) and unused When the user performs a one-tap check-in from the peripheral Then exactly one check-in is recorded for the current day for the selected habit/room And the action completes end-to-end within 2.0 seconds P95 (local UI feedback within 700 ms) And no biometric/passkey prompt is shown And duplicate taps within 3 seconds are idempotent (no additional check-ins)
Expired/Invalid Token Silent Refresh via Paired App (Success Path)
Given the peripheral token is expired or invalid And the paired phone has network connectivity and the user is signed in When the user performs a one-tap check-in Then the paired app performs a silent, passkey-anchored session refresh and issues a new scoped token within 2.0 seconds P95 And the check-in completes using the refreshed token with no user prompt And telemetry records a silent_refresh_used=true flag for the attempt
Risk Detection Triggers Passkey Escalation on Phone
Given a check-in attempt from a peripheral occurs with any risk signal present (e.g., device mismatch, jailbreak/root detected, location shift > 500 km in 10 minutes, replayed token, >2 silent refresh failures within 60 seconds) When the attempt is evaluated Then the peripheral receives an escalate response And the paired phone displays a passkey/biometric prompt within 1.5 seconds P95 And the check-in is recorded only upon successful biometric assertion within 60 seconds And if the user cancels or fails authentication 3 times, no check-in is recorded and the peripheral shows an actionable error
Offline Watch/Lockscreen Check-in Queue and Later Sync
Given the peripheral is offline (no internet and/or not connected to the phone) When the user performs a one-tap check-in Then the check-in is stored in a tamper-evident offline queue with a signed payload, nonce, and timestamp And the UI shows Queued within 700 ms And the queue retains up to 10 pending check-ins per device for up to 24 hours When connectivity resumes, queued check-ins are submitted in order and validated; valid items are recorded and duplicates are ignored And items older than 24 hours are rejected with an expired status and surfaced to the user
Capability Scoping: Peripherals Restricted to Check-in Only
Given a peripheral credential or token exists When the peripheral attempts any action other than POST /check-in (e.g., profile read, settings change, room join/leave) Then the request is blocked client-side and rejected server-side with 403 And no biometric prompt is displayed as a bypass And the attempt is logged with scope_violation=true and contains no sensitive data
Secure Enclave Storage and Revocation/Rotation
Given extension credentials are created for a peripheral Then keys and tokens are stored in hardware-backed secure storage (iOS Secure Enclave/Keychain with kSecAttrAccessibleAfterFirstUnlock, Android StrongBox/Keystore with StrongBox if available) And keys are non-exportable and cannot be restored to a different device via OS backup And derived peripheral signing keys are rotated at least every 7 days and immediately on sign-out or device unlink When the user signs out, uninstalls, or revokes the device, all stored credentials are wiped and subsequent check-ins fail with 401
Sensitive Action Biometric Gate
"As a security-conscious user, I want biometric prompts only when performing sensitive changes so that everyday actions remain smooth yet secure."
Description

Introduce step-up authentication that invokes a biometric prompt only for sensitive operations (e.g., email/SSO linking, passkey management, payment details, device trust changes). Use platform biometrics (Face ID/Touch ID/Android BiometricPrompt/WebAuthn user verification) with clear copy and cancellable flows. Normal actions, including check-ins and navigation, remain uninterrupted. Add configurable risk rules to trigger step-up on anomalies (e.g., new device, risky IP, impossible travel). This balances frictionless daily use with strong protection for high-impact changes.

Acceptance Criteria
Email/SSO Linking Requires Biometric Verification
Given an authenticated user attempts to link a new email or SSO provider from Account > Linked Accounts When the user initiates the link action Then a platform biometric prompt (Face ID/Touch ID/Android BiometricPrompt/WebAuthn user verification) is invoked before any account-change request is sent And linking proceeds only upon successful biometric verification completed within 60 seconds And on cancel or after 3 failed biometric attempts, the linking action is aborted with no changes to linked identities And an audit event auth.step_up is recorded with action=link_identity, result=success|failure|cancel, userId, deviceId, platform, timestamp, ip, riskReason And the user sees action-specific copy stating the reason for verification and a visible cancel option
Passkey Management Protected by Biometrics
Given a user selects Add Passkey or Remove Passkey in Security > Passkeys When the user confirms the action Then a platform biometric prompt is invoked (mobile: Face ID/Touch ID/Android BiometricPrompt; web: WebAuthn with userVerification=required) And on success, the passkey list updates within 1 second to reflect the new state, with server response 200 And if biometrics are unavailable, a supported fallback is offered (device passcode/PIN on mobile; platform authenticator PIN or re-auth screen on web) And on cancel or failure, no passkey is added or removed and the user is returned to the passkeys screen And an audit event auth.step_up is recorded with action=manage_passkey and result=success|failure|cancel
Payment Details Update Step-Up Authentication
Given a user edits or adds payment details in Billing When the user attempts to save the changes Then a platform biometric prompt is required before tokenization or server submission occurs And on successful verification, payment details are tokenized and saved, returning a success state and updated masked payment method And on cancel or failure, no payment information is persisted or sent to the processor, and the form remains editable And the prompt occurs at most once per save attempt to prevent repeated prompts And an audit event auth.step_up is recorded with action=update_payment and result=success|failure|cancel
Device Trust Change Biometric Gate
Given a user toggles Trust This Device or selects Revoke Trusted Device(s) in Security > Devices When the user confirms the trust-state change Then a platform biometric prompt is invoked prior to applying the change And on success, the device trust state is updated server-side and reflected in the UI within 2 seconds And if revoking other devices, those sessions are invalidated within 60 seconds while the current device remains signed in And on cancel or failure, no trust-state changes are applied And an audit event auth.step_up is recorded with action=change_device_trust and result=success|failure|cancel including affectedDeviceIds
Risk-Based Step-Up on Anomalous Context for Sensitive Actions
Given a sensitive action (link identity, manage passkey, update payment, change device trust) And the request context matches a configured risk rule (e.g., new device, IP reputation <= threshold, geo-velocity > configured km/h within 1h) When the user initiates the sensitive action Then step-up verification is required even with a valid session, with copy indicating additional verification due to unusual activity And on success, the action proceeds normally; on cancel/failure, the action is aborted with no changes And risk policy thresholds are configurable server-side and take effect within 5 minutes of change And audit events include riskReason and riskScore for the action And normal (non-sensitive) actions are never gated by risk-based step-up
Uninterrupted Check-Ins and Navigation (No Step-Up)
Given a user performs normal actions (check-ins including lockscreen and watch, opening Auto-Room Links, navigation between tabs) When these actions are executed under any session state, including after background refresh Then no biometric prompt is displayed and the action completes without additional friction And zero auth.step_up events are recorded for actions in {check_in, navigate, open_auto_room} And added latency attributable to auth checks does not exceed 50 ms at P95 for these actions
Step-Up Prompt Copy, Cancellation, and Accessibility
Given any step-up prompt is invoked for a sensitive action When the prompt is shown Then the user sees action-specific reason text and a visible cancel option, localized to the app language (at minimum EN and ES) And screen readers announce the reason and options; focus returns to the initiating control on cancel And only one prompt is visible at a time (no stacked dialogs), and cancelling returns the user to the previous screen with no partial changes And telemetry records prompt_shown, prompt_canceled, prompt_success with timestamps for UX analysis
Session Integrity, Revocation, and Device Management
"As a user, I want visibility and control over my signed-in devices with automatic protection from suspicious activity so that my account stays secure without losing my streak flow."
Description

Maintain a server-side session ledger per device capturing credential ID, last refresh, client metadata, and trust state. Provide APIs and in-app UI for viewing active devices and revoking individual or all sessions. On revocation, immediately invalidate access/refresh tokens and notify extensions (widgets/watch) to drop scoped tokens. Implement anomaly detection (sudden ASN changes, repeated refresh failures) to require step-up or force sign-out. This gives users control and preserves safety without undermining the always-signed-in experience.

Acceptance Criteria
View Active Devices and Session Ledger
Given the user is authenticated and has one or more active sessions across devices When they open Settings > Security > Active Devices Then the app fetches GET /v1/sessions and displays all sessions sorted by last_refresh_at descending And each session shows: device_name, platform, app_version, credential_id_suffix (last 6 chars), last_refresh_at (local time), last_ip_asn_name, last_ip_country, trust_state And the count displayed equals the number of records returned And no session shows the full credential_id or full IP address And the p95 latency from tap to rendered list is <= 800 ms on Wi‑Fi with up to 50 entries
Revoke Individual Device Session (Not Current)
Given the user views Active Devices and selects a non-current device When they tap Revoke and pass biometric step-up Then the server marks that session trust_state=revoked and invalidates its access and refresh tokens within 1 second And the revoked device receives a push "drop_scoped_tokens" and ceases authenticated calls within 5 seconds And any subsequent API call from that device returns 401 Revoked And the device disappears from the Active Devices list within 5 seconds And an audit event "session.revoked" with session_id and reason=user_action is recorded
Revoke All Other Sessions
Given the user has multiple active sessions including the current device When they choose Sign out of other devices and pass biometric step-up Then the server revokes all sessions except the current device within 1 second And all other devices and extensions drop tokens within 5 seconds and receive 401 on the next call And the Active Devices list updates to show only the current device within 5 seconds And an audit event "sessions.revoke_all_others" is recorded
Revoke Current Device Session
Given the user is on their current device's session detail When they tap Revoke this device and pass biometric step-up Then local access and refresh tokens are deleted immediately and the app transitions to a signed-out state within 1 second And the server marks the session trust_state=revoked and invalidates tokens within 1 second And Auto-Room Links, lockscreen check-ins, and watch actions are disabled until re-authentication And the next foreground launch prompts passkey re-auth and creates a new session record upon success
Anomaly Detection: Step-Up or Force Sign-Out
Given an active session performing silent refresh When the ASN changes to a different ASN or country between two consecutive refreshes within 10 minutes, or device metadata (platform/app_version) regresses unexpectedly Then trust_state becomes suspicious and the next privileged action prompts biometric/passkey step-up And on successful step-up, trust_state returns to active and silent refresh resumes And if 3 consecutive refresh failures occur within 10 minutes or the user cancels step-up 3 times, the session is revoked and the user is signed out, with an in-app alert explaining the reason
Session APIs: Contract and Security
Given authorized requests with a valid access token When calling GET /v1/sessions Then the response is 200 with an array of session records containing: session_id, device_name, platform, app_version, credential_id_suffix, last_refresh_at, last_ip_asn_name, last_ip_country, trust_state And p95 latency <= 300 ms and results are scoped to the caller's account only When calling POST /v1/sessions/{session_id}/revoke or POST /v1/sessions/revoke_all_others Then the response is 200, the operation is idempotent, and revocations propagate to token introspection within 1 second And unauthorized callers receive 401 and cross-account access attempts receive 403
Offline Revoked Device Behavior
Given a device is offline when its session is revoked server-side When the device next attempts any API call Then the call fails with 401 Revoked and the client deletes local tokens within 1 second And the user is shown a re-auth prompt before any sensitive or background action can proceed And widgets/watch extensions deny actions until re-auth and display a "Sign in again" state

Alias Keys

Spin up separate, pseudonymous passkey profiles for different rooms or audiences and switch with a tap. Keep personal identity, creator presence, and team roles cleanly siloed while enjoying the same instant sign-in. Works with Pseudonym Picker, Avatar Veil, and Time Blur for maximum privacy.

Requirements

Alias Passkey Enrollment & Management
"As a privacy-conscious creator, I want to set up separate alias keys for my public and team rooms so that I can sign in instantly without revealing or mixing identities."
Description

Support creation and management of multiple pseudonymous passkey profiles per account using WebAuthn platform authenticators (Face/Touch ID, Android biometrics). Each alias key stores no PII, is device-bound, and maps to local, end-to-end encrypted metadata (alias label, pseudonym, avatar, privacy toggles). Provide flows to create, rename, set default, and delete aliases with clear warnings and key rotation for room bindings. Detect passkey availability, fall back gracefully, and ensure instant sign-in across app entry points. Integrate with Pseudonym Picker on creation and persist alias-scoped preferences. No private keys leave the device; servers only store public credentials per alias.

Acceptance Criteria
Create Alias Passkey with Pseudonym Picker
Given a signed-in user on a device with a WebAuthn platform authenticator available When the user taps "Create New Alias", completes the biometric prompt, selects a pseudonym and avatar, and sets privacy toggles Then a new alias passkey credential is registered and bound to the device via WebAuthn And the server stores only the alias public credential (credential ID and public key), with no PII And local alias metadata (label, pseudonym, avatar, privacy toggles) is saved encrypted on-device And the creation flow completes and displays success within 2 seconds of biometric confirmation
Switch Alias and Instant Sign-In Across Entry Points
Given a user has a default alias configured When the app is opened via cold start, deep link from a push, or share sheet Then the session is established using the default alias without additional prompts within 500 ms And the visible pseudonym/avatar reflect the default alias across the landing surface When the user taps Switch Alias and selects a different alias Then the session rebinds to the selected alias within 1 second and alias-scoped preferences become active immediately
Rename and Set Default Alias
Given at least one existing alias When the user renames an alias label Then the updated label is displayed consistently across the alias list, switcher, and room headers without altering the pseudonym unless explicitly changed When the user marks an alias as default Then subsequent sign-ins across all app entry points use that alias automatically And only one alias is marked as default at any time
Delete Alias with Room Binding Rotation
Given an alias A that is bound to one or more rooms When the user initiates deletion of alias A Then the app lists all impacted rooms and requires explicit confirmation And the user must select a replacement alias per room or choose to leave the room When the user confirms Then room bindings are atomically rotated to the selected aliases (or memberships are exited) and alias A credentials are deregistered locally and server-side And sign-in using alias A is no longer possible And if any rotation step fails, the operation is rolled back and a clear error is shown
Passkey Availability Detection and Graceful Fallback
Given a device or browser without an available WebAuthn platform authenticator (unsupported, locked out, or permissions denied) When the user attempts to create or use an alias passkey Then the app detects unavailability before prompting biometrics and displays clear guidance And alias creation is disabled while other authentication options configured by the product are offered And existing signed-in sessions remain usable And a non-PII telemetry event is logged for diagnostics
Local Metadata Encryption and Privacy Toggles Enforcement
Given an alias with label, pseudonym, avatar, privacy toggles, and preferences Then the alias metadata is stored only on-device encrypted with the OS keystore and is unreadable to other apps And no alias metadata is transmitted to the server or network services When the user switches aliases Then Avatar Veil and Time Blur settings apply immediately and persist across app restarts
Server Stores Only Public Credentials per Alias
Given an alias is created Then the server persists only the publicKeyCredentialId, public key, and required attestation metadata for that alias And the server rejects requests containing PII fields for alias creation or update And private keys never leave the device and cannot be exported When authenticating with the alias Then WebAuthn assertion succeeds using the stored public credentials and fails if the credential is not registered
One-Tap Alias Switcher
"As a remote worker, I want to switch my active alias with one tap when moving between rooms so that my check-ins and reactions always use the right identity."
Description

Deliver a persistent, context-aware UI control to switch the active alias with a single tap from the room header, composer, and check-in confirmation surfaces. Display the current alias (name + avatar veil) and prevent identity leakage by ensuring all outbound actions (check-ins, reactions, comments) are scoped to the selected alias. Provide pre-join alias selection for rooms, haptic feedback on switch, and guardrails to block mid-post switches that could cause misattribution. Persist last-used alias per context and expose quick-jump to manage aliases.

Acceptance Criteria
Single-Tap Alias Switcher Across Key Surfaces
Given the user is on a room header, the composer, or the check-in confirmation surface and has at least two aliases available When the user performs a single tap to select an alternate alias via the switcher control on that surface Then the active alias changes with no additional confirmation, the control updates to show the new alias name and avatar veil, and the visible UI on that surface reflects the new alias within 200 ms And the switcher control is persistently visible and tappable on all three surfaces
Outbound Actions Scoped to Selected Alias
Given alias A is the active alias When the user submits a check-in, adds a reaction, or posts a comment Then the action is attributed to alias A in all user-facing UI and backend payloads, with no personal identity metadata present (e.g., real name, primary account ID) And after switching to alias B, subsequent actions are attributed to alias B while previously submitted actions remain attributed to alias A
Pre-Join Alias Selection Gate for Rooms
Given the user attempts to enter a room where no active alias is set for that room context When the pre-join screen is shown Then an alias picker is required before entry, with the last-used alias for that room preselected if available or a privacy-safe default suggested if not And upon confirming an alias, the room opens with that alias active; cancelling or dismissing keeps the user out of the room
Mid-Post Switch Guardrails to Prevent Misattribution
Given the user is composing a post or comment, or has a check-in submission in progress When the user attempts to switch aliases before the submission completes Then the switch action is blocked, the current alias remains active for the pending submission, and a non-destructive notice explains that mid-post switching is disabled And after the submission completes or is discarded, alias switching is re-enabled
Contextual Persistence of Last-Used Alias Per Room
Given the user selects alias X while in room R from any supported surface When the user returns to room R or relaunches the app later Then alias X is the active alias by default across the room header, composer, and check-in confirmation surfaces for room R And selecting a different alias in room S does not change the active alias for room R
Haptic Feedback on Successful Alias Switch
Given the device supports haptic feedback and app haptics are enabled When an alias switch succeeds Then a single light haptic feedback is emitted once per successful switch And no haptic feedback is emitted on blocked or failed switch attempts
Quick-Jump to Manage Aliases from Switcher
Given the user opens the alias switcher control When the user selects Manage Aliases Then the app navigates to the alias management screen within two taps from the originating surface, and the back action returns the user to the same context and surface state And any alias created, edited, or deleted in management reflects in the switcher upon return without requiring an app restart
Room-Scoped Identity Binding
"As a user, I want each room to remember which alias I use so that I never accidentally post with the wrong identity."
Description

Enable per-room default alias selection with sticky rules and policies. On first join, prompt users to choose or create an alias; thereafter, auto-apply the bound alias for all room activity. Support admin-required policies (e.g., forced anonymity, Time Blur required, avatar veils only) and user overrides where allowed. Handle edge cases such as removed aliases, revoked keys, or transferred room ownership by prompting reassignment without revealing prior identities. Ensure bindings are stored locally with encrypted sync and never expose cross-room mappings server-side.

Acceptance Criteria
First Join Alias Selection Prompt
Given a signed-in user joins a room for the first time When the room loads before any posting or check-in is possible Then a modal prompts the user to choose an existing alias or create a new alias, including Pseudonym Picker, Avatar Veil, and Time Blur controls subject to room policy And the selected/created alias is bound as the room’s default for that user And joining actions are blocked until an alias is selected And the binding persists across app restarts and devices after secure sync is enabled
Sticky Room Alias Auto-Application
Given a user has a bound alias for a room When the user performs any room-scoped action (check-in, reaction, post, invite, or mention) Then the action is attributed to the bound alias without exposing other identities And the UI displays the bound alias avatar/handle consistently And no identity chooser is shown unless the user explicitly opens the switcher And upon re-entering the room on any device, the bound alias is preselected
Admin-Enforced Anonymity and Privacy Policies
Given a room admin configures policies (e.g., Forced Anonymity ON, Time Blur = configured minutes, Avatar Veils Only) When a member joins or attempts to change their alias or privacy settings Then options that violate policy are disabled or hidden And attempts to proceed out of compliance are blocked with a policy error message And Time Blur is enforced so published timestamps are rounded to the configured blur window And forced anonymity prevents selection of any non-pseudonymous alias or exposure of personal profile metadata
User Override Within Allowed Policies
Given a room policy allows alias switching by members When a user opens the identity switcher and selects a different compliant alias Then the new alias becomes the bound default for that room And subsequent actions use the new alias without requiring re-authentication beyond passkey assertion And the previous alias remains intact for its other rooms with no cross-room mapping displayed And a local-only audit entry is recorded without transmitting previous/new alias linkage to the server
Handling Removed Alias or Revoked Passkey
Given the bound alias for a room is deleted locally, its passkey is revoked, or the credential becomes unavailable When the user next opens the room or attempts any action Then the user is prompted to reassign a compliant alias before continuing And historical content remains attributed to the former alias without revealing any linkage And the reassignment flow does not display or transmit the prior alias identity And if the user cancels, room actions remain blocked until reassignment completes
Ownership Transfer and Policy Change Rebinding
Given room ownership is transferred or policies are changed to stricter settings When members next open the room Then members whose current alias violates new policies are prompted to select a compliant alias And existing compliant bindings remain unchanged And neither the new nor old owner can view cross-room identity mappings for any member And no server-side export or admin view reveals prior alias selections or linkages
Local E2EE Binding Storage and No Server Cross-Room Mapping
Given an alias-room binding is created or updated When the app persists and syncs the binding Then the binding is stored only on-device and synced using end-to-end encryption with keys not accessible to the server And server APIs neither accept nor return any identifier that links the user’s aliases across rooms And network payloads and server logs contain only room-scoped alias identifiers and signatures, never a global user identifier And a simulated server data dump lacks any table or field that correlates aliases across different rooms
Pseudonym Picker, Avatar Veil, and Time Blur Integration
"As a creator, I want my alias to have its own pseudonym, avatar, and time blur settings so that I can control how much of myself I reveal in each audience."
Description

On alias creation and edit, integrate Pseudonym Picker to generate/select a unique handle, apply Avatar Veil settings, and configure Time Blur granularity. Enforce consistency across UI surfaces (profiles, check-ins, live reactions, notifications) so that alias privacy settings propagate everywhere. Provide per-alias presets and room-level overrides where required by policy. Ensure camera and media pipelines respect veil settings, and timestamp rendering respects blur rules in feeds, exports, and web previews.

Acceptance Criteria
Alias Creation: Picker + Veil + Time Blur Setup
- Given I create a new alias, When I open the creation flow, Then I can select or generate a unique pseudonym via Pseudonym Picker before saving. - Given a selected pseudonym, When I attempt to save a duplicate within my account, Then I am blocked with a uniqueness error and suggestions. - Given veil and time-blur choices, When I adjust settings, Then a live preview updates for profile, check-in, and notification surfaces before save. - Given I save the alias, When the operation completes, Then the persisted alias includes pseudonym, veil, and time-blur settings and is retrievable via API and UI within 2 seconds.
Privacy Propagation Across UI Surfaces
- Given an alias with Avatar Veil and Time Blur, When I view profiles, check-ins, live reactions, and in-app notifications, Then the pseudonym, veiled avatar, and blurred timestamps are consistently displayed on all surfaces. - Given the same alias, When a push or email notification is sent, Then notification title/body and media use the alias handle, veiled avatar, and blurred timestamp with no legal name, un-veiled media, or precise time. - Given the alias participates in multiple rooms, When I switch rooms, Then the alias privacy representation remains consistent unless a room policy override applies.
Per-Alias Presets and Room-Level Overrides
- Given an alias preset (veil, time blur) and a room policy requiring stricter settings, When I join or post in that room, Then the effective settings are the stricter of preset vs policy. - Given an override is applied, When I view the composer or my check-in, Then an explicit "Room Override" indicator shows the effective settings and which rule enforced it. - Given I attempt to lower settings below the room policy, When I save, Then the change is rejected or constrained to the minimum allowed and the UI communicates the constraint. - Given an override event occurs, When I review audit logs, Then an entry records alias id, room id, prior vs effective settings, timestamp, and actor.
Camera and Media Pipeline Respect Avatar Veil
- Given an alias with Avatar Veil enabled, When I capture or upload media, Then previews, thumbnails, and stored assets reflect the veil with no un-veiled frames displayed to others. - Given media is saved or exported, When examining file metadata, Then EXIF/GPS/device identifiers are stripped and filenames contain no personal identifiers. - Given live reactions or video check-ins, When streaming, Then real-time processing applies veil consistently with under 200ms added latency and zero leak of un-veiled frames to recipients.
Timestamp Rendering Respects Time Blur (Feeds/Exports/Web)
- Given an alias with Time Blur set to hour/day/week, When I view feed items and room timelines, Then timestamps are bucketed to the selected granularity and never reveal a more precise time. - Given I export activity or share a web preview, When inspecting rendered timestamps, Then the same granularity is applied and server-side rendering does not bypass blur. - Given client and viewer time zones differ, When rendering blurred timestamps, Then the bucket is computed server-side using room policy and UTC rules to ensure consistent results for all viewers.
Alias Switching Is Atomic and Privacy-Safe
- Given I have multiple alias keys, When I switch aliases with a tap, Then subsequent check-ins, reactions, and notifications use the selected alias's pseudonym, veil, and time-blur settings. - Given I switch while composing, When I confirm the switch, Then the composer updates identity, media veil, and timestamp preview before posting. - Given network interruptions during switch, When the switch fails, Then the prior alias remains active and no mixed-identity content is posted; an error is shown.
Privacy and Anti-Linkability Safeguards
"As a privacy-focused user, I want confidence that my aliases cannot be linked through metadata so that my personal identity remains siloed."
Description

Implement defense-in-depth to prevent cross-alias correlation: isolate analytics identifiers per alias, avoid reusing device tokens or headers across aliases, randomize webhook and export IDs, and redact PII in logs. Store alias-to-user mappings only on-device with E2EE sync; servers see distinct public credentials and alias IDs. Provide a privacy review checklist, in-app privacy explainer, and one-tap export/delete for any alias. Use platform secure storage (Secure Enclave/StrongBox) for key material and encrypt all alias metadata at rest and in transit.

Acceptance Criteria
Per-Alias Analytics Isolation
Given a device with two aliases (A and B) for the same user When analytics events are emitted under alias A Then the payload and transport include an alias_analytics_id unique to A and exclude any identifier shared with alias B (e.g., device_id, installation_id) Given a switch from alias A to alias B When a new analytics session starts Then a new session_id is generated and no prior alias/session/user ID is reused Given backend analytics storage When querying events for aliases A and B Then no join key exists that links A and B and the count of distinct alias_analytics_id equals the number of aliases used
No Reuse of Device Tokens and Headers Across Aliases
Given two aliases on the same device When API requests are sent under each alias Then Authorization tokens, X-Client-Instance-ID, and any device fingerprint headers are distinct per alias and not reused Given push notification registration per alias When registering A and B Then each registers a distinct token or alias-scoped registration; the server does not return the same token for both Given runtime storage isolation When switching between aliases Then cookie jars, local storage namespaces, caches, and persisted session data are isolated per alias and never shared
Randomized Webhook and Export Identifiers
Given alias A initiates a webhook callback or data export When the identifier or token is generated Then it has >=128 bits of entropy, is URL-safe, non-sequential, and unique; it is not shared with any other alias Given two aliases on the same account When generating 1000 webhook/export IDs per alias Then there is no shared prefix or deterministic correlation across aliases Given long-lived signed URLs or webhook secrets When 24 hours elapse Then alias-scoped signing keys rotate; prior tokens for one alias are revoked without affecting others
PII Redaction in Logs and Telemetry
Given client and server logging are enabled When PII fields (e.g., email, phone, real name, IP, device serial) are processed Then logs store only "[REDACTED]" or an irreversible salted hash and never the raw value Given a synthetic user with seeded PII When running a 24-hour soak test Then automated log scans report 0 PII occurrences with severity=blocker Given telemetry event schemas When emitting events Then only allowlisted fields are serialized; attempts to log non-allowlisted PII are rejected with an error
On-Device Alias-to-User Mapping with E2EE Sync
Given an account with aliases A and B When inspecting server APIs and data stores Then no alias-to-user mapping is retrievable; only alias_public_credential and alias_id exist server-side Given E2EE backup/restore to a second device When the user restores with their recovery method Then aliases reappear after local decryption; network traces show only opaque ciphertext, and the server never learns the mapping Given a red-team request to export alias mappings from the server When executed Then the result is empty and documented as not possible by design
Hardware-Backed Key Security and Encryption at Rest/In Transit
Given a device with Secure Enclave/StrongBox When creating an alias Then its private keys are generated as non-exportable, hardware-backed keys with successful attestation; attempts to export fail with a platform error Given a device lacking hardware-backed keystore When creating an alias Then the app warns the user and stores keys in the OS keystore with non-exportable flags; policy is recorded in telemetry without PII Given data at rest and in transit When inspecting local storage and network traffic Then alias metadata is encrypted at rest (e.g., AES-GCM or platform equivalent) and all transport uses TLS 1.2+ with modern ciphers; key material is never transmitted
Privacy UX: Checklist, Explainer, and One-Tap Export/Delete per Alias
Given a release candidate When the privacy review checklist is executed Then all items are checked with links to evidence; CI blocks release if any required item is unchecked Given an alias settings screen When the user opens the Privacy Explainer Then it clearly describes anti-linkability measures and data handling in plain language (<= 8th grade readability) and links to the policy Given the user taps Export for alias A When export completes Then a machine-readable file (JSON or ZIP) is available within 5 minutes containing only alias A data; verification confirms no data from other aliases Given the user taps Delete for alias B When confirmed Then alias B credentials are revoked immediately; server data for B is purged within 24 hours; related APIs return 404; backups are scheduled for purge within 30 days with audit logs
Cross-Device Alias Key Sync and Recovery
"As a multi-device user, I want my alias keys and settings available on all my devices so that I can sign in instantly without recreating profiles."
Description

Leverage platform passkey sync (iCloud Keychain, Google Password Manager) for WebAuthn credential availability across devices. Sync alias metadata (labels, pseudonym, veil/blur settings, room bindings) via end-to-end encryption with user-controlled recovery keys. Provide device add, revoke, and lost-device flows, QR-based device-to-device bootstrap as a fallback, and conflict resolution when multiple devices modify alias metadata. Ensure consent screens clearly explain what syncs and maintain zero-knowledge server posture.

Acceptance Criteria
Passkey Sign-in on New Device with Synced Credential
Given the user has a StreakShare passkey synced via iCloud Keychain or Google Password Manager and no local E2E keys on the device When the user taps "Sign in with Passkey" Then WebAuthn authentication succeeds without requiring a password And the device is added to the user's device list with a recognizable device name and platform And the app prompts to restore encryption keys via Recovery Key or QR bootstrap if keys are absent And no alias metadata is displayed until decryption keys are present
Real-time E2E Alias Metadata Sync Across Devices
Given Device A and Device B are signed in, online, and in foreground When the user updates an alias label, pseudonym, avatar veil, time blur setting, or room binding on Device A Then the change is visible and decrypted on Device B within 10 seconds And network payloads for alias metadata contain only ciphertext and non-sensitive headers (no plaintext labels, pseudonyms, veil/blur values, or room IDs) And the server stores only encrypted blobs (verified via API response schemas) And a sync status indicator confirms "Up to date" on both devices
Recovery Key Generation, Verification, and Restore
Given the user enables end-to-end encrypted sync When the app generates a recovery key (e.g., 24-word phrase or QR-exportable key) Then the key is displayed once with copy/secure-save options and an "I saved this" confirmation gate And a post-generation verify step requires re-entering or scanning the key to confirm accuracy And when the user restores on a new device by providing the recovery key, all alias metadata decrypts successfully And after a recovery key rotation, the previous key no longer decrypts new backups while existing devices remain functional
Device List Visibility and Revocation Propagation
Given the user opens Settings > Devices Then each device entry shows device name, platform, last active time, and key status When the user revokes a device Then the revocation propagates to the revoked device within 60 seconds if online, or on next connection And the revoked device is signed out and can no longer decrypt alias metadata And remaining devices continue to sync normally
QR Device-to-Device Bootstrap Fallback
Given Device A is signed in with valid E2E keys and Device B is authenticated via passkey but lacks E2E keys When Device B displays a pairing QR and Device A scans it Then an encrypted, proximity-limited session transfers E2E keys without server-readable plaintext And the QR session expires if not completed within 60 seconds or after one use And Device B can decrypt alias metadata within 5 seconds after completion And both devices show a "Keys transferred" confirmation
Concurrent Alias Metadata Edit Conflict Resolution
Given Device A and Device B edit the same alias metadata within a 30-second window while online When both changes are synced Then the system applies deterministic field-level last-writer-wins using device-signed timestamps And both devices converge to identical alias metadata within 10 seconds And the losing change is logged locally with a "conflict resolved" notice and a link to activity details And no data loss occurs for unrelated fields edited on either device
Sync Consent and Zero-Knowledge Disclosure
Given a first-time enablement of sync on any device When the consent screen is shown Then it clearly lists what syncs (alias labels, pseudonyms, veil/blur settings, room bindings) and that they are end-to-end encrypted with a user-held recovery key And it states the server has zero knowledge of alias contents and cannot recover the key if lost And the user must explicitly "Agree" before sync starts; "Learn more" opens documentation; "Not now" leaves sync disabled And consent is recorded with timestamp and device identifier and is retrievable in settings

Recovery Circle

Add a second device (or a trusted partner device) as a backup authenticator through a passkey handshake. If you lose a phone, approve access from a linked device or scan a recovery QR to regain your account—no passwords or support tickets required. Keeps control in your hands without exposing identity.

Requirements

Passkey Device Linking
"As a security-conscious user, I want to add a second device via a passkey handshake so that I can regain access if I lose my primary phone without relying on passwords or support."
Description

Implement a passkey-based handshake (WebAuthn/FIDO2) to link an additional personal device to a user’s StreakShare account. Allow initiation from either device, perform mutual challenge–response, bind a device-bound credential, and register the device as a recovery authenticator. Support iOS, Android, and desktop platform authenticators, handle cancellations and errors gracefully, and confirm linkage with an in-app success state. Do not require passwords, SMS, or email; store only public keys and minimal device metadata. Integrate prompts into onboarding and account settings to encourage setup without adding friction to daily habit flows.

Acceptance Criteria
Link second device initiated from primary device
Given the user is authenticated on Device A with a platform authenticator available When the user selects Link another device in Account Settings Then the app generates a single-use WebAuthn cross-device handshake with a challenge that expires in 120 seconds and displays a QR containing only a nonce and session binding id Given Device B scans the QR from Device A When Device B performs WebAuthn credential creation with user verification required and signs the server challenge Then the server validates rpId and origin, accepts attestation type none or direct, and issues a mutual challenge for Device A to sign Given Device A receives the mutual challenge When the user completes user verification on Device A Then the server verifies both signatures, binds Device B as a device-bound credential, and persists only public key, hashed device identifier, platform type, createdAt, lastSeen Then both devices display a success state within 3 seconds and the new device appears in Recovery Circle with a default nickname and rename option
Link second device initiated from new device
Given Device B has no active session When the user selects Link to my existing account and chooses Approve from another device Then Device B displays a QR code and a 6-character short code that expire in 120 seconds Given the user opens Account Settings on Device A and selects Approve a new device When Device A scans the QR or enters the short code from Device B and completes user verification Then mutual challenge-response completes, Device B registers a platform passkey with user verification required, and Device B is added as a recovery authenticator without creating an active session unless the user chooses Sign in now
Recovery approval from linked device
Given the user has at least one linked recovery device When the user initiates Recover account on Device C Then the app offers Approve from a linked device without requiring password, SMS, or email When Device C requests approval Then each online linked device receives an approval prompt within 5 seconds showing requester device or browser, OS, approximate city, time, and a one-time phrase When the user approves on one linked device and completes user verification Then Device C completes WebAuthn challenge verification, gains account access, lastSeen for the approving device updates, and an audit log entry is created When the request is denied or no response occurs within 120 seconds Then Device C shows Request expired with a retry option and no session is created
QR-based recovery when primary device is unavailable
Given the user is locked out on Device C and has a linked recovery device When Device C displays a recovery QR and short code valid for 120 seconds Then scanning the QR with a linked device initiates mutual challenge requiring user verification on the linked device When verification on the linked device succeeds Then Device C is granted account access, the recovery token is invalidated, and it cannot be reused When the short code is entered on the linked device instead of scanning Then the same security checks and outcomes apply with identical rate limits If no linked device is reachable Then only QR and short code methods are offered and there is no fallback to password, SMS, or email
Cancellation, timeout, and error handling
When the user cancels on either device at any point Then both devices transition to a cancelled state within 2 seconds and all pending challenges are invalidated server-side If no user verification completes within 120 seconds Then the flow times out, shows a neutral explanatory message, and allows a safe retry after 5 seconds If the device or browser is unsupported for WebAuthn Then the UI offers QR cross-device approval and does not offer password, SMS, or email Server rate limits linking to 5 attempts per user per 5 minutes and 20 per IP per hour, returning HTTP 429 with a retry-after header on excess Error messages avoid revealing whether a user exists and never include PII
Security, storage, and revocation
All WebAuthn operations require user verification with UV true and enforce RP ID streakshare.app and expected origins only Challenges are 32 bytes of cryptographically random data, single-use, and stored with a 120-second TTL The server stores only publicKey, credentialId, AA GUID if provided, device nickname, platform type, createdAt, updatedAt, lastSeen, and a salted hash of a device fingerprint; no biometric data or raw attestation certificates are persisted Replay, downgrade, and cross-origin attempts are rejected with HTTP 400 and are audited When a device is revoked by the user Then the credential is immediately invalidated, removed from the Recovery Circle list, and cannot approve recovery or link flows All actions are recorded to an immutable audit log with timestamp, actor device, action, and outcome
Cross-platform support and low-friction UX
Linking and recovery flows function on iOS 16.4+ Safari, iPadOS 16.4+, Android 10+ Chrome, macOS 13+ Safari or Chrome, and Windows 10+ Edge or Chrome using platform authenticators or QR fallback When caBLE v2 or equivalent cross-device transport is available Then it is used automatically; otherwise QR plus short code fallback is offered The onboarding prompt to set up a recovery device appears after first successful sign-in, is dismissible, and never blocks daily check-ins or habit flows Telemetry shows time to complete linking is 45 seconds or less at p50 and 120 seconds or less at p95 All UI strings are localized for en-US and support accessibility with screen reader labels, sufficient contrast, and proper focus management during multi-device flows A success confirmation appears on both devices with device nickname, recovery role, and a Manage devices action
Trusted Partner Approval
"As a user who collaborates with a friend, I want to add a trusted partner who can approve my recovery so that I’m not locked out if all my devices are unavailable."
Description

Enable users to designate a trusted partner (another StreakShare user) who can approve recovery requests. Provide an invitation and acceptance flow, establish a partner-bound public key relationship, and require local biometric/secure unlock for approvals. Display only minimal identity cues (display name and avatar hash) to avoid exposing sensitive information. Support configurable approval thresholds when multiple partners are added, explicit timeouts, clear deny/approve actions, and immutable audit logs. Partners have no access to private data and cannot assume account control beyond approving recovery.

Acceptance Criteria
Partner Invitation and Key Binding
- Given user A is authenticated and user B is an existing StreakShare user, When A sends a trusted partner invite to B, Then an invitation with unique ID and status "Pending" is created and visible to both A and B. - When B accepts the invite via in-app flow and completes a passkey ceremony, Then the system stores B's public key bound to A's account and the relationship status becomes "Active". - When B declines or the invite reaches the configured expiration T_invite, Then no key material is stored and the invitation status is "Declined" or "Expired" respectively. - Then an audit event is appended for invite sent, accepted, declined, or expired with timestamp and actor identifiers. - Then A can revoke the relationship at any time, changing status to "Revoked" and preventing future approvals while retaining audit history.
Recovery Approval with Biometric/Device Unlock
- Given owner A has initiated an account recovery request R, When partner B opens the approval screen for R, Then B sees Approve and Deny actions and the request expires at configured timeout T_recovery. - When B taps Approve, Then an OS-level biometric or secure device unlock is required before submission; if unavailable, fallback to device PIN/passcode is required. - When B successfully completes local auth and submits Approve, Then the approval is signed with B's passkey and server verification against stored public key succeeds, marking B's decision as "Approved". - When B taps Deny, Then Deny submits without granting access, recording decision "Denied" and notifying A. - When local biometric/auth fails 3 consecutive times, Then Approve is disabled for R until the app is re-authenticated.
Minimal Identity Cues Display
- Given any screen or API related to trusted partner invites, approvals, or relationships, When presenting the counterparty, Then only display name and avatar hash are shown or returned. - Then no email, phone number, physical address, exact username/handle, device identifiers, or other PII are shown or returned in payloads or logs for these flows. - Then UI snapshots and API schemas for these endpoints pass automated checks that assert only {display_name, avatar_hash} are present as identity cues.
Multi-Partner Threshold and Timeout Handling
- Given A has n trusted partners and sets approval threshold k (1 ≤ k ≤ n), When A saves the setting, Then the persisted configuration reflects k-of-n and is retrievable. - Given a recovery request R is created with threshold k and timeout T_recovery, When partners submit approvals, Then R is granted immediately upon receiving k distinct valid approvals before T_recovery. - When T_recovery elapses before k approvals are received, Then R status becomes "Expired" and further approvals are rejected with error "RequestExpired". - When A changes k or partner list while R is pending, Then R is closed with status "Superseded" and cannot be approved further. - When any partner submits Deny, Then it is logged but does not reduce the count of required approvals; if all partners deny, Then R status becomes "Denied".
Immutable Audit Logging
- Given any trusted-partner lifecycle event (invite sent/accepted/declined/expired, partner revoked, threshold changed, recovery requested, approval/denial submitted, grant issued), Then an append-only audit entry is written with fields {event_type, actor_id, target_id, request_id, ISO8601_UTC_timestamp, device_hash, event_signature}. - Then audit entries are immutable: attempts to update or delete an entry are rejected and detected by a verifiable hash chain linking successive entries for the account. - Then the owner can retrieve a chronological audit view that matches the event sequence produced during tests, with no gaps or reordering beyond eventual consistency guarantees.
Partner Permission Scope Restrictions
- Given B is a trusted partner of A, When B is signed into their own account, Then B cannot view A's private data, sessions, settings, or content; such requests return HTTP 403 "Forbidden". - When B approves a recovery for A, Then no session, token, or credential that grants B access to A's account is issued. - When B attempts to change A's partner list, thresholds, or security settings, Then the action is blocked with HTTP 403 and an audit entry "UnauthorizedAttempt" is recorded. - Then after approval, B can only see their own decision state (Approved/Denied/Expired) for request R and cannot access the recovery grant or device-binding secrets.
Recovery QR Challenge
"As a user who just replaced my phone, I want to scan a secure recovery QR to rebind my account so that I can get back into StreakShare quickly and safely."
Description

Provide a recovery QR flow that encodes a short‑lived, signed recovery challenge. Allow a trusted device or partner to present the QR for a new device to scan, initiating a secure, passwordless account rebind. The QR carries no PII, expires quickly, and always requires explicit approval from a linked device or partner before completion. Support deep links on iOS/Android, an accessibility fallback with a short alphanumeric code, and integrity checks before binding a new passkey on the recovering device.

Acceptance Criteria
QR challenge is signed, short‑lived, and PII‑free
Given a user on a linked or partner device requests a recovery QR When the server issues the QR payload Then the payload contains only: ver, challengeId, nonce (>=256 bits), exp (<=5 minutes from issuance), and server signature And the signature is verifiable by the server’s public key and invalid if any field is altered And no PII (email, phone, username, display name, device name, IP) exists in the payload or QR image metadata And the challenge expires no later than 5 minutes after issuance and is rejected after expiry And each challenge is single‑use and is invalidated immediately upon first approval or explicit cancel
Explicit approval gate on linked device or partner before rebind
Given a new device submits a valid recovery QR scan or code When the server notifies the selected linked or partner device Then the rebind completes only after the approver explicitly taps Approve within 120 seconds And tapping Deny cancels the request and invalidates the challenge for all devices And if no response occurs within 120 seconds, the request times out and cannot be resumed And approvals are accepted only from devices registered in the user’s Recovery Circle for the target account And the approver is shown request context (requesting device OS, approximate region, time) without revealing PII to the requesting device
Cross‑platform deep link handling for recovery scans
Given a user scans the recovery QR using an iOS or Android camera/scanner When the deep link is opened Then on iOS, a Universal Link routes directly to StreakShare and opens the Recovery Rebind screen And on Android, an App Link routes directly to StreakShare and opens the Recovery Rebind screen And if the app is not installed, the user is routed to the correct store page; after install and returning to the link, the pending challenge resumes And the deep link token is short‑lived and becomes unusable after expiry or completion And opening the link on desktop shows instructions and the short alphanumeric code for manual entry
Accessibility fallback via short alphanumeric code
Given camera access is unavailable or the user prefers manual entry When the trusted device displays the recovery code Then a 10‑character, case‑insensitive alphanumeric code using unambiguous characters (A–H, J–N, P–Z, 2–9) is shown And the code maps 1:1 to the same signed challenge and shares the same expiry (<=5 minutes) and single‑use constraints And the recovering device accepts the code within 5 minutes and rejects it thereafter And manual entry attempts are rate‑limited to 5 attempts per 10 minutes per device/IP; exceeding the limit invalidates the challenge And error states are announced via accessibility APIs (VoiceOver/TalkBack) with clear messages
Integrity checks and anti‑replay before passkey binding
Given a recovering device initiates account rebind from a scanned QR or code When the server validates the challenge Then the server verifies signature validity, freshness (<=5 minutes), and that the challengeId is unused And the server requires device integrity proof when available (Android Play Integrity or iOS DeviceCheck) and rejects devices failing basic integrity And requests occur over TLS 1.2+; replayed submissions of the same challengeId are rejected with 409 and do not progress binding And if client/server time skew exceeds 2 minutes, the user is prompted to retry and binding is not performed
Passwordless rebind completes with new passkey on recovering device
Given the approval gate is satisfied for a valid recovery challenge When the recovering device proceeds to credential creation Then the device is prompted to create a platform passkey via WebAuthn (discoverable credential) And on success, the user is signed in on the recovering device without any password entry And the new device is added to the account’s trusted devices; the challenge is permanently invalidated And if passkey creation fails or is aborted, no account rebind occurs and the challenge remains pending until expiry or cancel And confirmations are shown on both devices; the requesting device never receives PII from the approver
Device Management Dashboard
"As a user, I want a clear dashboard to manage my linked devices and partners so that I stay in control of who can help me recover access."
Description

Create an account settings dashboard to manage Recovery Circle relationships. List linked devices and trusted partners with last seen, device names, and capabilities; allow revoke, rename, and configuration of recovery thresholds. Show pending invitations and active recovery requests with real-time status. Gate sensitive actions behind local authentication and provide clear warnings and confirmations before revocation. Integrate educational copy to explain Recovery Circle and encourage healthy setup without disrupting habit-tracking workflows.

Acceptance Criteria
View and Audit Linked Devices and Trusted Partners
- Given I am an authenticated user with at least one linked device and one trusted partner, When I open Settings > Recovery Circle > Device Management Dashboard, Then I see a unified list segmented by Devices and Trusted Partners displaying: name, type, capabilities (Approve Recovery, Initiate Recovery), last seen (relative and absolute timestamp), and platform icon. - Given the list is visible, When the backend returns more than 20 items, Then the UI paginates or virtualizes to keep initial render under 500ms and scrolling at 60fps on a mid‑range device. - Given the list is visible, When no devices or partners are linked, Then I see an empty state with a CTA to add a device/partner and a link to documentation. - Given the list is visible, When a device or partner’s status changes server‑side (e.g., last seen, capability update), Then the UI reflects the change within 3 seconds via real‑time channel or 10‑second polling fallback. - Given auditability is required, When I expand an item, Then I can view audit details (linked on, last activity, actor of last change) with timestamps in ISO 8601 and local time display.
Revoke Linked Device with Local Authentication and Confirmation
- Given I am on the dashboard and a device is selected, When I tap Revoke, Then I am prompted for local authentication (biometric or device PIN) and revocation is blocked after 3 failed attempts for 60 seconds. - Given I pass local authentication, When I confirm the revocation on a warning dialog that clearly explains loss of recovery capability and session invalidation, Then the device is removed from the linked list within 5 seconds and can no longer approve recovery. - Given revocation succeeds, Then all active tokens for that device are invalidated server‑side within 10 seconds, and an audit log entry with actor, device ID, and timestamp is recorded. - Given network failure occurs during revocation, When I retry, Then the action is idempotent and the final state is consistent with exactly‑once revocation. - Given the device was currently viewing the dashboard on another session, When revocation completes, Then that session shows a “device revoked” banner and remove capabilities immediately.
Rename Linked Device and Persist Across Sessions
- Given I am on a device detail panel, When I edit the device name and submit, Then the new name persists and is reflected across all sessions within 5 seconds. - Given validation rules, When I enter a name longer than 40 characters or containing only whitespace, Then the Save action is disabled and an inline error explains the constraint. - Given I attempt to reuse an existing device name, Then the system allows it (names are labels, not identifiers) and no error is thrown. - Given the rename is in progress, When the request takes longer than 3 seconds, Then a non‑blocking progress indicator is shown; on failure, an inline error with Retry appears without losing my input.
Configure Recovery Approval Thresholds with Validation
- Given I have N eligible approvers (linked devices and trusted partners with approval capability), When I open Recovery Thresholds, Then I can set a required approvals value T where 1 ≤ T ≤ N. - Given I attempt to set T > N or N = 0, Then Save is disabled with an explanation and a link to add approvers. - Given reducing T lowers security risk, When I decrease T, Then I must pass local authentication and confirm on a risk warning modal before changes apply. - Given increasing T could lock me out if insufficient approvers are active, When I set T such that T > number of currently reachable approvers, Then I see a warning but may proceed after confirmation. - Given I save a new threshold, Then it applies to new recovery requests only, and the effective T is displayed in the dashboard header and persisted across sessions.
Manage Pending Invitations (Resend, Cancel, Expiry)
- Given I have sent invitations, When I open the Pending Invitations panel, Then I see each invitee’s identifier, invited timestamp, expiry timestamp (7 days), and status (Pending, Accepted, Expired, Canceled). - Given an invitation is Pending, When I tap Resend, Then the invite is reissued, rate‑limited to once per hour per invitee, and the latest sent timestamp updates. - Given an invitation is Pending, When I tap Cancel and confirm, Then the invitation becomes Canceled within 3 seconds and can no longer be accepted. - Given an invitation reaches expiry, When the expiry time passes, Then the status auto‑updates to Expired within 3 seconds without requiring a page refresh. - Given network failure on Resend or Cancel, Then I see an inline error and can Retry; successful retries are idempotent and do not duplicate invitations.
Monitor Active Recovery Requests with Real-Time Status
- Given there is an active recovery request on my account, When I open the Active Recovery Requests panel, Then I see a card showing initiator, created time, required approvals T, approvals collected C, and statuses (Awaiting, Approved, Denied, Canceled, Expired). - Given approvers respond, When an approval or denial is recorded, Then C and status update in the UI within 2 seconds via real‑time channel or 10‑second polling fallback. - Given I initiated the recovery, When I tap Cancel, Then the request transitions to Canceled within 5 seconds and is no longer actionable by approvers. - Given a request reaches T approvals, Then the status transitions to Approved and a success banner appears; if a denial threshold is met (policy), Then the status transitions to Denied with explanation. - Given no activity for the configured timeout window, Then the request auto‑expires and is moved to History with a timestamped reason.
Educational Copy and Non-Disruptive UX Safeguards
- Given it is my first visit to the dashboard, When the page loads, Then I see concise educational copy explaining Recovery Circle with a Learn More link, and I can dismiss it; the dismissal is remembered for future visits. - Given I am in an active habit check‑in flow elsewhere in the app, When I open and close the dashboard, Then my check‑in state is preserved and not reset, and navigation back returns within 300ms on a mid‑range device. - Given accessibility requirements, When navigating the dashboard, Then all interactive elements are keyboard accessible, have ARIA labels, and meet WCAG 2.1 AA color contrast. - Given sensitive actions (revoke device, lower threshold) exist, When I attempt them, Then they are gated by local authentication and a clear warning modal; non‑sensitive actions (rename, resend invite) are not gated. - Given localization, When the app language changes, Then all dashboard copy (including educational content and warnings) is localized and numbers/dates respect locale formatting.
Key Rotation and Session Invalidation
"As a user who just recovered my account, I want old sessions revoked and keys rotated so that my account stays secure after a lost device."
Description

Upon successful recovery, rotate recovery and session keys, bind the new device’s passkey as primary, and invalidate tokens and sessions on lost or untrusted devices. Re-encrypt account data at rest with updated keys where applicable and propagate changes across services. Preserve user streaks, rooms, and notifications while guiding the user through a post-recovery checklist to confirm device inventory and optionally notify the partner of completion.

Acceptance Criteria
Post-Recovery Key Rotation and Primary Passkey Binding
- Given an account is recovered via Recovery Circle and the user confirms ownership on a trusted device, When recovery is finalized, Then a new recovery key is generated and the old recovery key is invalidated immediately. - Given recovery completes, When updating authenticators, Then the new device's passkey is set as the primary authenticator and any previous primary on the lost device is revoked. - Given the update, When querying the account, Then the primary device ID reflects the new device and is effective within 5 seconds. - Given a failure occurs during binding, When rollback is triggered, Then the account remains accessible on the recovering device and no partial state persists.
Session and Token Invalidation on Lost Devices
- Given the user marks a device as lost during recovery, When the process completes, Then all refresh tokens, access tokens, and active sessions associated with the lost device are invalidated within 10 seconds. - Given invalidation, When the lost device attempts API calls, Then the request is rejected with 401 and a device_revoked error code. - Given push channels, When invalidation occurs, Then push notification credentials for the lost device are revoked and cannot receive notifications. - Given multiple lost devices are selected, When complete, Then all selected devices' sessions are invalidated; unaffected devices remain signed in.
Re-encryption of Data at Rest with Rotated Keys
- Given key rotation is triggered, When re-encrypting data at rest, Then user account secrets, session salts, and recovery artifacts are re-encrypted with the new keys; content data that is not key-bound remains unchanged. - Given re-encryption starts, When monitoring progress, Then 100% of targeted records are re-encrypted or queued within 60 seconds for accounts under 1GB. - Given re-encryption completes, When verifying, Then old keys are securely destroyed or sealed and cannot decrypt data. - Given a partial failure, When retry policy executes, Then re-encryption resumes without data loss and completes within 3 retries.
Cross-Service Propagation of Rotation State
- Given distributed services (auth, profile, rooms, notifications), When recovery completes, Then rotation state and new key material are propagated to all services via signed events with an eventual consistency SLA of 30 seconds. - Given propagation, When any service receives an event older than the current version, Then it rejects it and requests the latest state. - Given a network partition, When connectivity resumes, Then services reconcile and converge to the latest version with no conflicting primary passkey. - Given propagation completes, When calling any service, Then the user's requests are authorized only with new tokens.
Preservation of Streaks, Rooms, and Notifications
- Given recovery completes, When the user opens the app, Then streak counts, room memberships, and scheduled notification preferences are unchanged. - Given that sessions were invalidated on lost devices, When notifications are sent, Then only trusted devices receive them; notification cadence remains intact. - Given database re-encryption due to key rotation, When verifying the user's timeline, Then no check-in history is missing or reordered. - Given a discrepancy is detected, When integrity checks run, Then the system restores from the latest consistent snapshot and reports the incident.
Post-Recovery Checklist and Partner Notification
- Given recovery succeeded, When the post-recovery checklist is displayed, Then it lists current device inventory, shows which devices were revoked, and requires user acknowledgement for each step before completion. - Given the user chooses to notify a partner, When the checklist is completed, Then a secure in-app notification is sent to the partner device with a timestamp and no PII beyond the user's display name. - Given the user declines notification, When the checklist completes, Then no message is sent and the choice is logged. - Given the checklist is incomplete, When the user tries to exit, Then the app prompts to finish or save progress without blocking access to core functionality.
Security and Anti‑Abuse Controls
"As a user, I want strong safeguards around recovery so that attackers can’t exploit the process to take over my account."
Description

Implement defense-in-depth for recovery workflows: rate limit recovery attempts per account and per device, detect anomalies using geo/IP/device signals, require biometric confirmation for approvers, and record signed audit logs for approvals and denials. Require at least one strong signal (linked device or trusted partner) to complete recovery with no email/SMS fallback. Provide user alerts for suspicious activity and a one-tap revoke for pending requests. Store minimal metadata, encrypt at rest, and include automated tests and security review for common threat models.

Acceptance Criteria
Rate-Limited Recovery Attempts
Given an account with 0 recovery initiation attempts in the past 24 hours, When 1 to 3 recovery initiation requests are made, Then all are processed normally. Given an account with 3 recovery initiation requests (success or failure) in the past rolling 24 hours, When an additional recovery initiation request is made from any device or IP, Then the request is rejected with HTTP 429 and message "Recovery attempts rate-limited" and no approver notifications are sent. Given a single device has made 5 recovery initiation requests in the past rolling 24 hours, When a 6th request is made from that device, Then the request is rejected with HTTP 429 and the device is backoff-silenced for 60 minutes. Given rate limit windows elapse (24 hours from first counted attempt), When a new recovery request is made, Then the request is processed and counters are reset for that account/device. Given concurrent recovery requests exceed limits and arrive within 100 ms, When processed, Then at most one is accepted and the rest are rejected, preventing race-condition bypass.
Anomaly-Driven Risk Gating for Recovery
Given a recovery initiation from a device with geolocation >500 km from the last approved device location within 1 hour or an IP ASN change from residential to data center or an unseen device fingerprint, When the computed risk score >= 70, Then classify the request as High Risk and require strong-signal approval (linked device passkey or trusted partner approval). Given a High Risk recovery request, When no strong-signal approval completes within 15 minutes, Then the request auto-expires and is denied and an alert is sent to the account owner's signed-in devices. Given a Low/Medium Risk recovery request (risk score < 70), When a strong signal is provided, Then recovery proceeds; When no strong signal is available, Then recovery is denied. Given a recovery attempt originates from a Tor exit node or known proxy/VPN list, When detected, Then classify as High Risk and suppress any recovery QR until a strong signal is present.
Biometric Confirmation on Approver Device
Given a linked device or trusted partner receives a recovery approval request, When the approver taps Approve, Then the OS biometric prompt appears within 2 seconds and approval is only accepted upon successful biometric within 30 seconds. Given the approver fails biometric 3 times or cancels, When the attempt ends, Then the approval is refused and the requester is notified of denial without revealing the approver’s identity. Given the approver device has no enrolled biometrics, When an approval request arrives, Then the device is not eligible to approve and the approval action is disabled with guidance to enroll biometrics. Given the approver device is locked, When an approval request arrives, Then the device must be unlocked and a biometric confirmation completed before approval is accepted.
Tamper-Proof, Minimal Audit Logging
Given any recovery approval or denial event, When it is recorded, Then an audit entry is created containing only: event_id, hashed account_id, hashed approver_device_id, requester_device_hash, request_id, decision, risk_score, truncated IP (/24 IPv4 or /48 IPv6), country code, timestamp (ms), and public_key_id. Given an audit entry is created, When persisted, Then it is signed with Ed25519 using the current logging key and stored append-only; signature verification succeeds for 100% of entries during nightly verification. Given a stored audit log entry is modified, When signature verification runs, Then tampering is detected and a security alert is raised within 5 minutes. Given audit data at rest (including backups), When inspected, Then it is encrypted with AES-256-GCM and keys are KMS-managed with rotation at least every 90 days.
Strong Signal Required; No Email/SMS Fallback
Given a user initiates account recovery, When the recovery options are presented, Then only "Approve on linked device" and "Trusted partner approval" are available and no email/SMS code options are shown. Given an API client invokes any email/SMS-based recovery endpoint, When called, Then the server responds 404 Not Found and no PII is logged. Given the account has neither a linked device nor a trusted partner configured, When recovery is attempted, Then the flow refuses to proceed and displays guidance to enroll a Recovery Circle without exposing identity details.
Suspicious Activity Alerts and One-Tap Revoke
Given a recovery attempt is classified High Risk or hits a rate limit, When the event occurs, Then push notifications are sent to all signed-in devices within 10 seconds and an in-app banner appears on next foreground. Given one or more pending recovery requests exist, When the user taps "Revoke all" from the alert, Then all pending recovery tokens are invalidated within 2 seconds and subsequent approvals for those requests are rejected with reason "revoked" and an audit entry is created. Given a revoke action is taken, When the original requester attempts to proceed, Then the UI immediately reflects the revocation and offers to start a new request subject to current rate limits.
Automated Tests and Security Review Sign-off
Given the recovery security feature set is implemented, When the CI pipeline runs, Then unit and integration tests cover at least rate limit enforcement, risk classification, biometric gating, audit log signing/verification, revoke flow, and "no email/SMS fallback", achieving ≥90% line coverage in recovery modules. Given security testing, When red-team scripts attempt replay, race-condition, device cloning, QR phishing/MitM, rate-limit bypass, and email/SMS fallback abuse, Then all attempts are detected or blocked as designed and test artifacts are stored in CI. Given release readiness, When a formal security review is conducted, Then a security engineer signs off with identified risks and mitigations and there are no open Critical or High findings.
Recovery Notifications and Timeouts
"As a user seeking recovery, I want timely approval notifications to my Recovery Circle so that the process is fast and transparent."
Description

Deliver real-time, actionable notifications for recovery events to linked devices and trusted partners via push and in-app banners. Include clear context (requesting device, time, approximate location), approve/deny actions, and a countdown timer with default expiration. Provide resilient delivery with retries and a secure pull mechanism if push fails, and surface outcomes to the requester without exposing additional partner identity beyond what is already shared.

Acceptance Criteria
Push Notification With Context and Countdown
Given a recovery request is initiated from Device A at time T with TTL provided by the server (default 10 minutes when unspecified) And Device B is a linked or trusted partner device (not the requesting device) When the request is created Then Device B receives a push notification within 5 seconds containing: requesting device label (as already shared), request time in recipient’s local timezone, approximate location (city and country or "Approximate area unavailable"), Approve and Deny actions, and a visible countdown starting at the TTL value And the notification is actionable (Approve/Deny) without opening the app where the OS supports it; otherwise, actions deep-link to the in-app banner And the countdown matches the server TTL and disables actions precisely at expiration
In-App Recovery Banner When Active
Given a pending recovery request exists for the recipient device within TTL When the recipient opens the app or foregrounds it Then a persistent, high-visibility in-app banner is displayed within 2 seconds showing: requesting device label (as already shared), request time (local), approximate location (city and country or fallback text), Approve and Deny buttons, and a live countdown And the banner is accessible (screen-reader labels for context and actions, buttons reachable via keyboard navigation) And only one banner per pending request is shown; banners are removed immediately upon approval, denial, or expiration
Resilient Delivery: Retry and Secure Pull Fallback
Given a recovery notification fan-out to eligible linked and trusted partner devices is attempted When push delivery to a device fails (provider error or no receipt) after up to 3 retries with exponential backoff (e.g., 0s, 15s, 60s) Then the system marks push as undelivered for that device and enables a secure pull fallback for that device And when the recipient app comes online within TTL, it authenticates with a device-bound credential and pulls pending recovery requests Then the in-app banner renders within 2 seconds of successful pull And the server stops further push retries after a successful pull acknowledgment And an audit record captures delivery path (push or pull) and timestamps
Approve Action Processes Recovery and Notifies Requester
Given Device B has a pending recovery request R with nonce N and remaining TTL When the user taps Approve before expiration Then the server verifies Device B is authorized for the account’s Recovery Circle and validates N exactly once And the server completes the recovery for the requester and invalidates all other pending actions for R And Device A (requester) receives a success notification/in-app banner within 5 seconds stating the request was approved, using only identifiers already shared (e.g., "Trusted partner" or existing device name) And any subsequent Approve/Deny attempts for R return "Already completed" without changing state
Deny Action Cancels Recovery and Notifies Requester
Given a pending recovery request R exists within TTL When a recipient taps Deny before expiration Then the server marks R as denied, invalidates N, and prevents any further approvals for R And Device A (requester) receives a notification/in-app banner within 5 seconds stating the request was denied without revealing any new partner identity details And any subsequent actions on R display "Request denied" and perform no state change
Automatic Timeout and Expiration Handling
Given a pending recovery request R with TTL When the countdown reaches zero without an approval or denial Then the server automatically expires R and disables Approve/Deny across all notifications and banners And the requester is notified that the request expired and must initiate a new recovery attempt And any late actions on R return "Expired" and are logged without changing state
Privacy Preservation in Notifications and Outcomes
Given recovery notifications and banners are presented to recipients Then they display only: requesting device friendly name if previously shared, request time (local), and approximate location at city-and-country granularity (no exact coordinates) And they must not display new personal identifiers such as partner name, email, phone number, or precise location beyond what is already shared in the app context When outcomes (approved/denied/expired) are surfaced to the requester Then the message uses only previously shared identifiers (e.g., "Trusted partner") and does not reveal additional partner/device identity or metadata

Widget Trust

After one biometric check, StreakShare opens a short trust window so lockscreen and watch actions complete instantly while you stay in flow. SafeTap Undo still protects against mis-taps, and sensitive actions always re-prompt. Ideal for Micro-Breaks and edge-of-drift saves under pressure.

Requirements

Biometric Trust Session Engine
"As a busy remote worker, I want to authenticate once and perform quick check-ins from my lockscreen or watch without repeated prompts so that I can stay in flow during micro-breaks."
Description

Implements a single biometric authentication to open a time-bound trust window that enables instant execution of permitted actions from lockscreen widgets and wearables. Manages session lifecycle (start, extend, expire) with an ephemeral, device-scoped token stored in secure hardware-backed storage. Auto-terminates on device lock state change, timeout, or manual revoke. Supports iOS/Android lockscreen widgets and watchOS/Wear OS interactions with consistent behavior. Provides fallbacks when biometrics are unavailable (e.g., passcode) and gracefully degrades when background execution limits apply. Ensures instant actions meet latency targets and are queued or retried if network conditions are poor.

Acceptance Criteria
Start Trust Session After Biometric Verification
Given a user is on a device with biometrics enrolled and initiates a trust session When biometric authentication succeeds once Then a device-scoped, ephemeral trust token is created in hardware-backed secure storage and associated with the current app install And the token includes an issued-at timestamp and an expiry timestamp And the token is not persisted across reboot and cannot be exported to other apps or devices And no network call is required to start the session And the trust window becomes active immediately for permitted actions from lockscreen widgets and wearables
Trust Session Expiry and Auto-Termination
Given the trust window duration is configured to 2 minutes for testing When the configured duration elapses without extension Then the trust token is invalidated and the session ends within 100ms of expiry And subsequent lockscreen and wearable actions require re-authentication Given an active trust session When the device transitions to a locked state, biometric settings change, or the user switches system users/profiles Then the trust session auto-terminates immediately and the token is purged Given an active trust session When the user manually revokes trust from the app or system surface Then the trust token is purged and all pending permitted actions require re-authentication
Trust Session Extension Rules
Given an active trust session with 20 seconds remaining When the app receives a valid extend request from a permitted action Then the expiry is extended by the configured increment without exceeding the maximum total window (e.g., 5 minutes) And the new expiry time is persisted in secure storage Given an active trust session at max allowed duration When an extend request is made Then the extend is rejected and the existing expiry remains unchanged Given the device becomes locked or the session is revoked When an extend request occurs Then the extend is rejected and no new token is issued
Instant Execution Latency for Permitted Actions
Given an active trust session When a permitted action is triggered from a lockscreen widget or wearable Then local UI acknowledgement (haptic/visual) is shown within 150ms p95 And SafeTap Undo affordance is available for 2 seconds without blocking the action And the server call is sent immediately; if online, action completion confirmation is received within 500ms p95 And if offline or high latency is detected, the action is queued idempotently within 200ms and marked as "Queued" until retried And queued actions retry with exponential backoff up to 3 times or until connectivity returns, whichever comes first
Sensitive Actions Require Re-Authentication
Given an active trust session When the user attempts a sensitive action (e.g., changing trust window settings, deleting account, modifying payment methods, enrolling/changing biometrics) Then the user is re-prompted for biometric or passcode before the action executes And if re-authentication fails or is canceled, no state changes occur and the action is not queued And audit logs record the re-auth requirement and outcome
Fallbacks When Biometrics Are Unavailable
Given biometrics are unavailable or fail after 3 attempts When the user initiates a trust session Then the user is prompted for device passcode/PIN as a fallback And upon successful fallback authentication, a trust session starts with the same capabilities and duration And if fallback is unavailable or fails, the session does not start and permitted actions continue to require authentication per action And error messaging is shown within 300ms indicating the reason and next steps
Cross-Platform Parity and Background Limits
Given supported platforms (iOS/Android lockscreen widgets; watchOS/Wear OS) When a trust session is active Then permitted actions behave consistently across platforms regarding eligibility, latency targets, SafeTap Undo, and re-auth for sensitive actions And the trust token remains device-scoped (not shared across devices or profiles) and inaccessible to companion apps Given background execution limits prevent immediate network calls When a permitted action is triggered Then the action is captured, marked as "Queued", and retried when allowed by the OS, without waking the app beyond OS budget And the user receives non-intrusive feedback (e.g., badge/state) indicating queued status on the widget/watch
Action Sensitivity & Re-prompt Matrix
"As a creator, I want high-risk actions to always ask for confirmation or re-auth so that I don’t accidentally harm my streaks or data while moving fast."
Description

Defines risk tiers for actions (e.g., low: check-in/react; medium: note edits; high: delete/reset) and enforces policy so that sensitive actions always re-prompt regardless of trust state. Provides a configurable mapping with safe defaults and server-delivered policy updates. Ensures UI clearly communicates when re-auth is required, and blocks execution until satisfied. Handles edge cases such as expired sessions during multi-step flows and logs policy decisions for auditability.

Acceptance Criteria
High-Risk Actions Always Re-prompt Despite Trust Window
Given a user initiates a high-risk action (delete/reset) from any surface (app, lockscreen widget, or watch) and any trust state When the action is invoked Then a re-auth prompt (biometric or device passcode) is shown before execution Given the re-auth prompt is shown When the user cancels or fails authentication Then the action is not executed and no data is changed Given the re-auth prompt is shown When the user successfully authenticates Then the action executes and a policy decision log is recorded with action=risk_high, trust_state, policy_version, auth_result=success
Medium-Risk Actions Respect Trust Window With Policy Overrides
Given the default policy and an active trust window When the user performs a medium-risk action (e.g., note edit) Then the action executes without re-auth Given the default policy and no active trust window When the user performs a medium-risk action Then a re-auth prompt is required before execution Given a server policy with force_reprompt=true for a specific medium action When the user performs that action Then a re-auth prompt is required regardless of trust window Given a medium-risk decision is made When logging occurs Then the log includes mapped_risk_tier=medium and policy_version
Low-Risk Actions Execute Instantly and Are Protected by SafeTap Undo
Given any trust state When the user performs a low-risk action (e.g., check-in or react) Then the action executes immediately without re-auth Given a server policy that requires prompt for a specific low-risk action When the user performs that action Then a re-auth prompt is shown before execution Given a low- or medium-risk action completes When SafeTap Undo is invoked within the configured undo window Then the original action is reverted without additional re-auth and no irreversible changes persist
Server-Delivered Policy Updates Override Local Defaults Safely
Given the app launches or the policy TTL elapses When the app fetches the server policy Then the new policy is cached with a version and applied to subsequent decisions within 5 seconds Given the server policy fetch fails When a decision is needed Then the last known policy is used; if none exists, safe defaults are used (high=re-auth always, medium=prompt if no trust window, low=no prompt) Given a decision is evaluated When logging occurs Then the policy_version used is included in the audit log
UI Re-auth Prompt Communicates Requirement and Blocks Action
Given a re-auth is required When the prompt is displayed Then it shows the action name, a reason (e.g., "Sensitive action requires re-auth"), and options to Continue or Cancel, and the underlying action remains blocked until resolved Given the user cancels the prompt When the prompt is dismissed Then no changes are applied and the UI shows a non-destructive notice Given the action is initiated from lockscreen or watch When re-auth is required Then an equivalent prompt or secure handoff to device auth is shown before execution
Multi-step Flow Handles Session Expiry and Resumes Safely
Given the user is in a multi-step flow for a sensitive operation and the trust window expires mid-flow When the user confirms the final step Then a re-auth prompt is required before any irreversible changes occur Given the user successfully re-authenticates mid-flow When the action proceeds Then no partial side effects were committed before authentication and the trust window is renewed per configuration Given the user fails or cancels re-auth mid-flow When the flow exits Then the system returns to a safe pre-action state with no data changes
Audit Logging of Policy Decisions Without Sensitive Data
Given any action decision is evaluated When the decision is logged Then the log includes: timestamp, user_id_pseudonymous, device_surface, action_type, mapped_risk_tier, trust_state, policy_version, auth_required (true/false), auth_result (success/fail/cancel), and decision_outcome Given logs are generated while offline When connectivity is restored Then logs are queued and transmitted within 60 seconds without loss or duplication Given privacy requirements When logs are stored Then no raw biometric data, note contents, or personally identifiable information are recorded
SafeTap Undo in Widgets and Watch
"As a user on the go, I want a brief window to undo a mis-tap from my watch or lockscreen so that accidental inputs don’t affect my streak."
Description

Integrates SafeTap Undo for all eligible widget and wearable actions initiated during a trust window. Presents a subtle, time-limited undo affordance with haptic feedback and accessible labels. Ensures operations are idempotent and reversible within the undo window, including offline scenarios where actions are queued. Handles race conditions between undo and background sync, and persists minimal state to allow reliable rollback across brief app or process restarts.

Acceptance Criteria
Lockscreen widget check-in with SafeTap Undo during trust window
Given the user has authenticated and the trust window is active And an eligible habit shows a Check In action in the lockscreen widget When the user taps Check In Then the check-in is executed immediately without additional authentication And a subtle inline Undo affordance appears for 5 seconds And a light haptic is emitted on action and on Undo affordance reveal And the Undo affordance is keyboard and screen-reader accessible with the label "Undo check-in" and a minimum 44x44pt touch target And additional taps on Check In during the 5-second window do not create duplicate check-ins When 5 seconds elapse without tapping Undo Then the Undo affordance dismisses and the committed state persists
Watch quick reaction with SafeTap Undo during trust window
Given the user has authenticated and the trust window is active on the paired watch And an eligible habit room shows a Reaction action on the watch When the user sends a Reaction Then the reaction posts immediately without additional authentication And a wrist haptic confirms the action and an Undo control appears for 5 seconds And the Undo control is voice-over accessible with the label "Undo reaction" And repeated Reaction taps during the 5-second window are idempotent (one reaction state only) When the user taps Undo within 5 seconds Then the reaction is removed locally and remotely, and a distinct haptic confirms the rollback
Sensitive action re-authentication from widget or watch
Given the trust window is active When the user initiates a sensitive action from a widget or watch Then a biometric or passcode prompt is shown before performing the action And if authentication fails or is canceled, no change occurs and no Undo is shown And if authentication succeeds and the action is reversible, an Undo affordance appears for 5 seconds with an accurate accessible label And sensitive actions never bypass re-authentication during the trust window
Offline eligible action queued with SafeTap Undo
Given the device is offline and the trust window is active When the user performs an eligible action from a widget or watch Then the action is applied locally and added to an outbound queue exactly once And an Undo affordance appears for 5 seconds with a confirming haptic When the user taps Undo within 5 seconds Then the queued item is removed, the local state is reverted, and no network call is sent when connectivity resumes When the user does not tap Undo Then upon connectivity restoration, exactly one request is sent and the server reflects a single applied action
Race handling between Undo and background sync
Given intermittent connectivity and an eligible action performed during the trust window And the action enters the 5-second Undo window When the client sends the action to the server during the Undo window And the user taps Undo before the 5-second window expires Then the final state on both client and server is the action not applied And at most one apply is recorded and at most one compensating rollback is executed And no duplicate states or oscillations are observable on any client after sync completes
Undo persistence across brief app or process restarts
Given an eligible action is taken from a widget or watch and the 5-second Undo window is active When the host app, widget process, or watch app restarts and resumes within the remaining Undo window time Then the Undo affordance and remaining countdown are restored And tapping Undo within the remaining time fully rolls back the action locally and remotely, or removes it from the offline queue if not yet sent When the Undo window expires while the process is down Then no Undo is offered on resume and the committed state persists And only the minimal state required for rollback is persisted and is cleared after window expiry
Undo affordance accessibility and haptic compliance
Given system accessibility settings are enabled When an eligible action is performed and the Undo affordance appears Then the affordance has an accurate accessible name and hint, correct control role, and is reachable in the expected traversal order And the touch target is at least 44x44pt and meets contrast ratio 4.5:1 in light and dark modes And text scales with Dynamic Type without truncation or overlap, and is localized for supported languages And haptic feedback respects system vibration settings and Reduce Motion preferences
Trust Window UX Indicators
"As a user, I want a clear but unobtrusive signal when trust is active and how long remains so that I know when instant actions will work."
Description

Provides unobtrusive visual and haptic indicators showing when trust is active and remaining duration (e.g., badge/halo, countdown ring) on widgets and watch complications. Adapts to light/dark modes, small form factors, and different widget sizes without clutter. Surfaces clear states for active, expiring, expired, and re-auth-required, including accessible announcements. Ensures indicators are consistent across platforms and do not reveal sensitive information on the lockscreen.

Acceptance Criteria
Lockscreen Active Trust Indicator with Countdown
Given a user has successfully authenticated and the trust window starts When the user views a lock screen widget or watch complication during the trust window Then an unobtrusive active state indicator (badge/halo) is shown with a circular countdown displaying remaining seconds and updates at least once per second And the indicator does not overlap or reduce the tappable area of the primary action below platform guidelines (iOS ≥ 44x44 pt, watchOS ≥ 44x44 px) And a single light haptic is emitted once at activation on devices with haptic engines And the indicator contains no habit names or sensitive content
Trust Expiring State Transition and Haptic Cue
Given the trust window is active When remaining duration is ≤ 5 seconds Then the indicator transitions to an expiring state with a color shift to the warning palette and a gentle pulse at 1 Hz for the final 5 seconds And an expiring haptic warning is emitted once at the start of the expiring window And an accessible announcement states “Trust ending in X seconds” without repeating more than once per second And no sensitive text is revealed during the expiring state
Expired/Re-auth State and Prompt Behavior
Given the trust window has ended When the user views a widget or complication Then the indicator visibly switches to an expired state (no countdown) and displays a neutral re-auth affordance (icon/label) And tapping the widget or complication triggers a biometric prompt within 300 ms And on successful re-auth, the indicator returns to active state with a fresh countdown And on cancel or failure, the indicator remains in expired state And no habit names or activity details are shown in expired or re-auth-required states
Accessibility Announcements and Reduced Motion
Given system accessibility is enabled When trust activates, enters expiring, or expires Then VoiceOver/TalkBack announces “Trust active, X seconds remaining”, “Trust ending in X seconds”, or “Trust expired — re-auth required” respectively And announcements are throttled to ≤ 1 per state change and no more than once per second And with Reduce Motion enabled, pulse animations are replaced by static color/weight changes with no motion And with Increased Contrast/Reduce Transparency enabled, outlines and text meet contrast requirements and avoid translucency
Cross-Platform Visual Consistency
Given supported surfaces (iOS Lock Screen/Home Screen widgets, watchOS complications, Android Home/Lock Screen widgets) When the trust state changes among active, expiring, expired Then iconography, color tokens, and animation cadence match the approved design specifications for each state across platforms And visual regression tests pass against golden master snapshots for each surface with tolerance ≤ 2 px and color delta E ≤ 2 And haptic semantics (light at activation, warning at expiring, none at expired) are consistent where device capabilities exist
Privacy: No Sensitive Data on Lockscreen/Complications
Given any lockscreen widget or watch complication is displayed When trust indicators are shown for any state Then no habit names, room names, participant avatars, counts, or recent activity details are rendered And text strings are limited to generic trust state and time remaining only And automated UI tests verify absence of user-provided strings from the habit title field in these surfaces And privacy review sign-off is recorded before release
Small Form Factors and Theme Adaptation
Given smallest supported widget sizes and watch complications (e.g., iOS small, watchOS circular/extra small) and light/dark modes When trust indicators render for all states Then minimum ring stroke ≥ 2 px and minimum icon size ≥ 12 px remain legible without clipping or truncation And contrast ratios meet WCAG 2.1 AA (≥ 4.5:1 for text/icons against background) in both light and dark themes And in Always-On Display modes, animations are disabled and replaced with static indicators to mitigate burn-in And no more than one indicator element (icon + ring) is shown to avoid clutter on smallest sizes
Device-Scoped Trust & Watch Bridging
"As a privacy-conscious user, I want trust limited to the device I authenticated and optionally extended to my paired watch so that convenience doesn’t compromise security."
Description

Scopes trust sessions to the authenticating device and prevents cross-device reuse. Optionally bridges trust to a paired watch using a short-lived tether token gated by secure channel, wrist detection, and proximity, with instant revocation when the phone locks or moves out of range. Provides user controls to enable/disable bridging and handles disconnected or multi-watch scenarios safely. Ensures no trust is shared to other signed-in devices or the backend.

Acceptance Criteria
Phone Trust Window Is Device-Scoped
Given the user completes a biometric authentication on the phone When the trust window starts (default 30s; configurable 15–60s per device policy) Then lockscreen and quick actions on that phone execute without re-prompt during the active window And non-sensitive actions expose SafeTap Undo with a 5s undo window per action And sensitive actions (e.g., account/security changes, payment changes, data export, deletion) still require biometric re-prompt even within the window And the trust window auto-expires at configured TTL or immediately on explicit user revoke
Optional Watch Bridging via Secure Tether Token
Given the user has enabled Watch Bridging on the phone And a paired watch is unlocked, on-wrist, and connected over an OS-encrypted channel within near proximity When the phone trust window starts Then a short-lived tether token is issued to the paired watch with TTL <= remaining phone trust time and capped at 30s And watch-side non-sensitive actions complete without re-prompt while the token is valid And the watch displays an in-session indicator while bridged trust is active And no token is issued if any precondition (setting enabled, on-wrist, secure channel, proximity) is not satisfied
Trust Isolation Across Devices and Backend
Given the user is signed in on multiple devices (e.g., phone A and phone B) When a trust window is active on phone A Then phone B remains untrusted and prompts for biometric before actions And no elevated trust token or flag is transmitted to or honored by the backend; server-side endpoints do not accept trust as a substitute for required auth And any attempt to use a watch tether token with a device other than the paired phone fails validation
Immediate Revocation on Lock, Distance, or Disconnect
Given a trust window and/or watch tether token is active When the phone locks, reboots, or the biometric context is invalidated by the OS Then local trust and any watch tether token are revoked within 1s When the secure channel drops or proximity leaves near range for >2s Then the watch tether token is revoked and subsequent watch actions require fresh auth When the watch reports off-wrist via wrist detection Then the tether token is revoked immediately
Safe Handling for Multi-Watch and Reconnect Scenarios
Given the user has multiple watches paired When bridging occurs Then trust bridges to only one active, connected watch; other watches receive no trust And if the active watch switches during the window, bridging does not transfer; new actions require fresh biometric and a new token; the prior token is revoked And if a watch disconnects and reconnects during the window, no automatic re-grant occurs; fresh biometric is required
User Controls, Consent, and Local Auditability
Given the user disables Watch Bridging in settings on the phone Then no tether tokens are issued and watch actions require their own auth Given the user enables Watch Bridging Then a one-time device-scoped consent/education screen is shown before first bridge And the bridging setting is device-scoped (does not sync to other devices) And toggling bridging off during an active window revokes any existing watch token immediately And local audit entries record trust start/stop, bridge grant/revoke, and revocation reasons without storing biometrics or PII, and are viewable by the user
Security Guardrails & Compliance
"As a user, I want strong security around trust windows so that my account remains safe even if my device is briefly accessible to someone else."
Description

Implements security best practices for trust sessions: hardware-backed key storage, ephemeral tokens, rate limiting and cooldowns on failed auth, and automatic session invalidation on biometric database changes. Adds threat modeling, platform policy alignment (Face ID/Touch ID/Android BiometricPrompt), and logging with privacy-by-design. Ensures no trust credentials are persisted server-side and provides red-team/pen-test acceptance criteria before broad rollout.

Acceptance Criteria
Hardware-Backed Keys and Biometric Policy Alignment
Given a device with secure hardware and enrolled biometrics, When the user completes biometric auth to start a trust window, Then a device key is generated or retrieved from a hardware-backed keystore/enclave and attested as hardware-backed. Given platform biometric policies, When biometric auth is requested, Then the system biometric API is used (no custom UI), with an explicit localized reason string and appropriate fallback per platform policy. Given a sensitive action, When attempted within an active trust window, Then a fresh biometric prompt is required and the action does not rely solely on the trust window. Given an unsupported or insecure environment (no hardware keystore or biometrics not enrolled), When a trust window start is attempted, Then the app blocks trust window creation and requires full authentication.
Ephemeral Token Lifecycle and Constraints
Given a successful biometric auth, When a trust window is opened, Then the client mints single-use, audience-bound ephemeral tokens with TTL ≤ 120 seconds and scope limited to non-sensitive actions. Given a token redemption, When the same token is presented again, Then the server rejects it as a replay with HTTP 401 and error code "token_replay". Given trust window expiry, When TTL elapses or the app is terminated, Then all unredeemed tokens are invalidated client-side and subsequent actions require re-authentication. Given inspection of token claims, When decoded, Then no PII or biometric data is present and claims include only sub, device_id hash, scope, exp, and nonce with a valid signature.
Rate Limiting and Cooldowns on Failed Auth
Given repeated failed biometric attempts, When there are 5 failures within 2 minutes, Then a 5-minute cooldown is enforced and a fresh biometric or device credential is required after cooldown. Given trust-token redemption, When more than 10 redemptions occur per user-device within 60 seconds, Then subsequent requests return HTTP 429 with error code "rate_limited" until the rate drops below threshold. Given trust window creation, When more than 3 trust windows are started by the same user-device within 10 minutes, Then further attempts are blocked for 10 minutes and require a new biometric.
Automatic Invalidation on Biometric Database Changes
Given the device's biometric enrollments change, When the app next resumes or an action is attempted, Then any active trust window is immediately invalidated and a fresh biometric is required. Given tokens minted before the biometric change, When presented to the server, Then they are rejected with HTTP 401 and error code "biometric_changed". Given OS signals of biometric database changes, When detected, Then local trust state and keys tied to the prior enrollment state are cleared within 1 second.
Privacy-by-Design Audit Logging
Given trust-related security events (open/close window, token mint/redeem/deny, auth failure), When they occur, Then logs record event type, timestamp, outcome, and pseudonymous identifiers (user_id hash, device_id hash) only, excluding PII, token values, and biometric data. Given log retention policy, When log entries reach 30 days old, Then they are automatically deleted and not retrievable via standard interfaces. Given role-based access control, When a non-privileged user requests security logs, Then access is denied and the attempt is itself audited; only authorized security roles with MFA can access. Given a privacy redaction test on sample logs, When evaluated, Then no raw IPs (only /24 masked), names, emails, or precise geolocation are present.
No Server-Side Persistence of Trust Credentials
Given backend data stores, When inspected, Then no trust tokens, biometric templates, or trust-window state are stored at rest. Given token verification, When the server validates a token, Then verification is performed via stateless signature checks and a short-lived in-memory replay cache (≤ token TTL) only, with nothing written to persistent storage. Given error handling and logging, When failures occur, Then logs omit token values and secret material and contain only non-sensitive identifiers and error codes.
Security Assurance: Threat Modeling and Red-Team Gate
Given the feature design, When threat modeling is performed, Then STRIDE categories are covered, assets and trust boundaries documented, and all High/Critical risks have mapped mitigations with owners and due dates. Given mitigation implementation, When validation tests run, Then all High/Critical risks are verified mitigated or explicitly risk-accepted by an authorized executive with rationale recorded. Given an external red-team/penetration test on the release candidate, When completed, Then there are zero open Critical/High findings; any Medium findings have mitigations or scheduled fixes; failing this blocks rollout. Given regression testing, When previously resolved findings are retested, Then no reoccurrence is observed across iOS, Android, and backend environments.
Settings, Telemetry, and Rollout Controls
"As a power user, I want to configure the trust window duration and allowed actions so that the feature matches my workflow and risk tolerance."
Description

Adds in-app settings to opt in/out, adjust trust window duration (e.g., 15–120 seconds), and choose which action tiers are allowed without re-auth. Provides feature flags for safe, staged rollout and A/B testing. Captures privacy-preserving telemetry on session starts, instant action success, undo usage, and re-prompt rates to quantify time saved and adherence impact. Includes regional compliance gates and kill switch for rapid disablement if issues arise.

Acceptance Criteria
Opt-In/Out Toggle Applies Immediately and Persists
Given Widget Trust is available in app settings When the user turns the Widget Trust toggle off Then trust window creation is disabled immediately and any active trust session is terminated And lockscreen and watch actions require biometric re-authentication regardless of recent auth And the opt-out state persists after app relaunch When the user turns the Widget Trust toggle on Then trust window behavior is enabled using the saved duration and tier selections
Trust Window Duration Range, Clamping, and Session Behavior
Given the user opens the trust window duration control When the user sets a value within 15–120 seconds Then the value is saved and used for the next trust session When the user sets a value below 15 seconds Then 15 seconds is saved and displayed When the user sets a value above 120 seconds Then 120 seconds is saved and displayed And changing duration during an active trust session does not alter the remaining time of that session
Action Tier Controls and Sensitive Action Re-Prompt Enforcement
Given action tiers Low, Medium, and Sensitive are available in settings When the user enables Low and Medium tiers for instant actions Then during a trust session, Low and Medium tier actions complete without re-authentication And Sensitive tier actions always re-prompt regardless of trust state When the user disables a tier Then actions in that tier require re-authentication even during a trust session And the above behavior is consistent on lockscreen widget and watch
Privacy-Preserving Telemetry and Opt-Out Honor
Given telemetry is opted in When a trust session starts Then an event trust_session_start is recorded with fields: session_id (random), platform, region_code, target_duration_s When an instant action succeeds during a trust session Then an event instant_action_success is recorded with fields: session_id, action_tier, latency_ms When SafeTap Undo is used Then an event undo_used is recorded with fields: session_id, action_tier When a re-authentication prompt is shown during a trust session Then an event re_prompt_shown is recorded with fields: session_id, action_tier And all telemetry events exclude biometric data and personal identifiers and are transmitted over TLS And when telemetry is opted out Then no further events are stored or transmitted from that point forward
Remote Feature Flags: Staged Rollout and Kill Switch Enforcement
Given remote configuration defines widget_trust_enabled with targeting by app_version, platform, region, and percentage rollout When a device matches targeting with percentage X Then the feature is enabled only if the device falls within X percent by deterministic bucketing When kill_switch=true is received from remote config Then the feature is disabled within 5 minutes, any active trust sessions end, and the settings UI reflects disabled state And if remote fetch fails Then the last known flag values are used until the next successful fetch
A/B Test Assignment Stickiness and Event Tagging
Given an experiment WidgetTrust_Duration_AB with variants A and B and a defined split is active in remote configuration When an eligible user fetches configuration without a prior assignment for this experiment_id Then the user is randomly assigned using deterministic bucketing and the assignment persists across app restarts until experiment_id changes And all related telemetry events include experiment_id and variant And users are not reassigned mid-experiment
Regional Compliance Gates and Suppressed Availability
Given a remote list of disallowed regions for Widget Trust is configured When the user's account region is in the disallowed list Then the feature is hidden or disabled regardless of local settings and telemetry for this feature is suppressed, except a one-time feature_unavailable_region event if telemetry is opted in When the regional configuration changes Then the new rules are enforced on-device within 5 minutes of the next config fetch

Prime Pair

Auto-selects two micro-habits based on your calendar gaps, energy patterns, and a 15-second preference check. Each pick shows a simple “Why this” note and offers one-tap swaps from a curated micro-template library (30–120 seconds). Reduces choice paralysis and ensures your Day‑1 habits feel relevant and doable.

Requirements

Calendar Gap Detection
"As a busy remote worker, I want the app to find short free moments in my day so that I can fit in quick wins without disrupting my schedule."
Description

Implements read-only connections to Google, Apple, and Outlook calendars to identify micro-gaps suitable for 30–120 second habits. Applies user time zone, working hours, and configurable “no-interrupt” windows; respects event privacy (no content storage) and buffers 1–3 minutes around meetings and commute blocks. Handles all-day events, focus sessions, overlapping invites, and travel time. Produces a ranked list of candidate windows for the current day and near-future windows (next 4 hours) for the selection engine. Supports cold-start without a connected calendar by inferring likely gaps from typical app usage times. Caches the day’s windows on-device for offline resilience and updates in near-real-time on calendar changes.

Acceptance Criteria
Read-Only Calendar Connections & Privacy Enforcement
Given a user connects any supported calendar with read-only scope When the initial sync completes Then only event time spans, busy/free status, time zone, and travel time metadata are cached on-device And event titles, descriptions, attendees, and locations are neither stored nor logged And no API calls create, modify, or delete calendar events And private events are treated as busy blocks And disconnecting the calendar revokes tokens and removes cached metadata within 10 seconds
Gap Extraction With Buffers and No-Interrupt Windows
Given the user's working hours and no-interrupt windows are configured And the buffer is set between 1 and 3 minutes inclusive When gaps are computed for today Then every candidate window duration is between 30 and 120 seconds inclusive And no candidate overlaps any event extended by the configured buffer or any no-interrupt window And overlapping or concurrent events, focus sessions, and travel time blocks mark those periods as busy
All-Day and Overlapping Event Handling
Given the calendar contains all-day events with busy/free availability When gaps are computed Then days with all-day busy events yield zero candidate windows during working hours And all-day free or tentative events do not block candidate windows And in any time range with overlapping invites, if any overlapping event is busy, that range yields no candidate window
Time Zone Changes, DST, and Cross-Day Travel
Given the user's time zone or DST offset changes or the device crosses time zones When the app is foregrounded or receives a time-zone change signal Then all candidate windows are recalculated within 10 seconds using the current local time zone And no candidate window falls outside the user's working hours in the new time zone And travel events with provided duration block the corresponding time plus buffer, including across day boundaries
Ranked Candidate Windows for Today and Next 4 Hours
Given gaps have been computed When the selection engine requests candidates Then a ranked list for the current day is returned, sorted by soonest start time then by longest duration And a separate list of near-future candidates covering the next 4 hours is returned And if at least two valid windows exist in the next 4 hours, the top two are available for Prime Pair And no duplicate or overlapping candidate windows appear in the lists
Near-Real-Time Updates and Offline Resilience
Given the app has cached today's windows on-device When a calendar event is created, updated, or deleted that affects availability Then affected candidate windows are updated within 10 seconds of receiving the change And if the device is offline, the last cached windows are served and marked as cached And upon reconnect, windows are reconciled within 10 seconds and cache is refreshed And cached data persists across app restarts for the current day only
Cold-Start Inference Without Connected Calendar
Given no calendars are connected And the user has at least one app session in the past 24 hours When gaps are inferred Then at least two candidate windows in the next 4 hours are produced within the user's working hours And all candidate windows meet the 30–120 second duration constraint And if the user has no usage history, default inference proposes two windows in the next 4 hours centered on mid-morning and mid-afternoon within working hours
Energy Pattern Modeling
"As a creator with variable energy, I want the app to learn when I’m most alert so that the suggested micro-habits match what I can realistically do."
Description

Builds an on-device model that predicts low/medium/high energy bands across the day using historical check-in timestamps, completion rates, and optional lightweight self-reported energy pings. Supports a privacy-first approach with aggregation and no raw personal data leaving the device. Provides hourly (or 30-min) energy scores with confidence intervals and a cold-start default profile that adapts after 3–5 days. Exposes an API for the selection engine and allows the user to opt in/out or reset the model. Handles time zone shifts and weekends vs weekdays with decay on stale data.

Acceptance Criteria
On-Device Privacy-First Modeling
Given the device is online and the energy model runs, When monitoring outbound requests during training and scoring, Then no payload contains raw timestamps, per-event identifiers, or user text and zero bytes of model input features leave the device. Given telemetry export is enabled, When any analytics are sent, Then the payload contains only aggregate counts/histograms (no individual records) and no fields can be joined to identify a single user. Given the user has opted out of Energy Modeling, When the scheduler reaches a training window, Then no model job executes and no telemetry related to the model is transmitted. Given a packet capture runs during model operations, When traffic is analyzed, Then there are no outbound requests from the model module other than permitted aggregate telemetry and update checks.
Energy Score Resolution and Confidence Intervals
Given the model is enabled, When requesting a 24-hour profile at 60-minute resolution, Then the API returns 24 intervals each containing {start, end, score∈[0,1], band∈{low,medium,high}, confidence∈[0,1], timezone}. Given a request at 30-minute resolution for the next 24 hours, When the API responds, Then it returns exactly 48 intervals with the same schema. Given a request for the current interval, When getCurrentEnergy() is called, Then a response with band and confidence is returned within 50 ms on a median device. Given invalid parameters (e.g., negative duration or unsupported resolution), When the API is called, Then it returns a structured error (code and message) and no partial data.
Cold-Start Default and 3–5 Day Adaptation
Given a new user with zero historical data, When the profile is requested, Then a default energy profile is returned aligned to the user’s current timezone with weekday/weekend differentiation and confidence ≤ 0.4. Given at least 3 distinct calendar days of activity and ≥ 6 total check-ins, When the profile is requested, Then the model status is "personalized" and the output is no longer the default profile no later than day 5. Given the user has 5 days of activity, When comparing the personalized profile to default, Then at least 30% of intervals differ in score by ≥ 0.1 or band changes in ≥ 4 intervals, indicating adaptation. Given the user resets the model, When the next profile is requested, Then the status reverts to "default" within 1 second and previous personalized parameters are not recoverable on-device.
Optional Energy Pings Integration and Graceful Degradation
Given the user submits a self-reported energy ping for the current interval, When the next profile is computed, Then the confidence for that interval increases by at least 0.1 (capped at 1.0) and the band reflects the ping within the next computation cycle (< 60 seconds). Given no energy pings are ever submitted, When profiles are requested, Then the API returns valid scores and bands based on check-ins and completion rates with no errors and no NaN/inf values. Given pings are disabled in settings, When the user attempts to submit a ping, Then the UI prevents submission and no ping data is stored. Given conflicting signals between pings and historical completion, When the profile is computed, Then the API still returns a single band and confidence without oscillation across adjacent intervals (≤ 1 band change per adjacent interval on average over a day).
Selection Engine API Contract
Given the selection engine calls getEnergyProfile(timeRange, resolution), When the call succeeds, Then the response contains an array of intervals with fields {start, end, score, band, confidence, timezone, modelStatus} and an HTTP/IPC success code. Given the selection engine calls getCurrentEnergy(), When invoked on a warmed model, Then the response is returned within 50 ms p50 and 150 ms p95 on target devices. Given invalid input (unsupported resolution, inverted time range), When the API is called, Then it returns error codes {INVALID_ARGUMENT, DISABLED, NO_DATA} without crashing. Given the user has opted out, When the selection engine calls any energy API, Then the API returns DISABLED with no profile data and logs no model access.
Time Zone Shifts, Weekday/Weekend Profiles, and Data Decay
Given the device time zone shifts by ≥ 1 hour, When the next profile is requested, Then interval boundaries reflect the new time zone within 5 minutes and historical data is re-indexed without duplication. Given a weekend day (Saturday/Sunday) vs a weekday, When the profile is requested, Then the model serves the corresponding weekend/weekday profile for today’s type. Given 14 consecutive days of inactivity, When the model computes a profile, Then contributions from data older than 14 days are down-weighted to ≤ 50% of their original weight; after 30 days, to ≤ 10%. Given a daylight saving time transition, When requesting a 24-hour profile that includes the transition, Then the API returns exactly 24 hours of coverage with no missing or duplicated intervals.
User Controls: Opt-In/Out and Reset
Given the user toggles Energy Modeling ON in settings, When consent is accepted, Then training and scoring are enabled and the model status becomes "default" within 1 second. Given the user toggles Energy Modeling OFF, When the setting is saved, Then scheduled jobs are canceled, scoring stops, and APIs return DISABLED with no personalized data. Given the user taps Reset Model, When confirmed, Then all model parameters and cached outputs are deleted within 2 seconds and the next query uses the default profile. Given the user opens the Energy Modeling settings, When the screen loads, Then it displays model status {Disabled, Default, Personalized} and the timestamp of the last update.
15‑Second Preference Check Flow
"As a user who values control, I want to quickly set my preference for the day so that the suggestions feel relevant without taking time."
Description

Delivers a lightweight, once-per-day micro-survey (≤15 seconds) that captures preferred focus areas (e.g., body, mind, workspace), contexts (seated/standing/quiet), and any constraints (no speaking, no equipment). Uses single-tap chips, haptics, and accessibility labels. Auto-saves partial input on timeout and defaults to the last known selections when skipped. Stores results on-device for 24 hours with clear edit/skip controls. Integrates with selection to bias picks and with analytics to measure completion and impact on adherence.

Acceptance Criteria
Once‑Per‑Day Prompt and 24h Storage
Given the user has not completed the preference check in the last 24 hours When the app first presents daily prompts Then the 15‑second preference check is shown once and will not auto‑prompt again for 24 hours Given a stored result exists that is less than 24 hours old When the user reopens the preference check manually Then previous selections are prefilled and editable, and no additional auto‑prompt occurs Given 24 hours have elapsed since the last saved result When the user launches the app Then the preference check is eligible to auto‑prompt again and the prior result is expired for biasing unless the user confirms it
Single‑Tap Input, Haptics, and Accessibility
Given the preference check is displayed When the user taps any focus, context, or constraint chip Then the chip toggles state with a single tap, with tap‑to‑feedback latency ≤ 100 ms and visual state change Given device haptics are enabled When a chip is toggled or the survey is submitted Then light haptic feedback occurs on toggle and medium haptic feedback occurs on submit Given a screen reader is active When navigating the chips Then each chip exposes an accessibility label announcing group, name, and selected state, has a minimum 44×44 pt touch target, and preserves logical focus order
Timeout Auto‑Save and Defaulting
Given the preference check is opened When 15 seconds elapse without the user submitting Then any selections made are auto‑saved, missing fields default to last known selections for the day (or to neutral defaults if none exist), and the sheet dismisses with a non‑blocking confirmation Given no prior selections exist When the user times out or skips Then neutral defaults are applied (no preferences/constraints) and stored for the day
Skip and Edit Controls
Given the preference check is displayed When the user taps Skip Then the flow dismisses immediately, the day’s preferences default to last known selections (or neutral if none), and an analytics event is recorded with completion_state=skipped Given a daily result exists When the user taps Edit from settings or the prompt entry point Then the previously saved selections are shown, changes can be made, and Save updates the stored result for the current day
Selection Bias Integration with Prime Pair
Given a saved daily preference exists When Prime Pair generates two micro‑habit picks Then both picks must satisfy all selected constraints (e.g., no speaking, no equipment) and match at least one selected context, and at least one pick aligns with a selected focus area Given no preferences are saved for the day When Prime Pair generates picks Then picks are drawn without constraint filters and without context/focus bias Given the user opens one‑tap swaps When the curated swap list is shown Then all swap options continue to honor the day’s constraints and context filters
Analytics Tracking and Adherence Correlation
Given the user interacts with the preference check When events occur (open, submit, timeout, skip, edit) Then an analytics event is queued with fields: preference_check_id (UUID), timestamp, completion_state, duration_ms, selected_focuses, selected_contexts, selected_constraints, accessibility_on, and network_status, with no PII Given the device is offline When an event is generated Then it is persisted on‑device and retried until sent, with a max retention of 72 hours Given a user completes a Prime Pair check‑in originating from a biased pick the same day When the check‑in event is emitted Then it includes the originating preference_check_id to enable impact analysis on adherence
Performance and Responsiveness SLA
Given a median target device (last two OS versions) When the preference check view is invoked Then time‑to‑first‑interaction ≤ 400 ms and submit‑to‑dismiss ≤ 300 ms on a good network, with full offline functionality and no crashes Given user taps any chip When feedback is rendered Then UI update occurs within 100 ms and frame drops do not exceed 5% during interaction
Prime Pair Scoring & Selection Engine
"As a time-pressed professional, I want the app to auto-pick two smart micro-habits right now so that I can act immediately without thinking."
Description

Combines calendar gaps, energy scores, daily preferences, recency/novelty, and user compliance history into a weighted scoring function to choose two distinct micro-habits. Enforces constraints: duration must fit the current gap, cognitive/physical load must match the energy band, and category diversity should prevent repetition fatigue. Provides deterministic selection per time window to avoid flicker, with a fallback set for no-calendar or low-confidence cases. Logs selection rationale and outcome metrics for learning and A/B experimentation. Exposes a service interface for the UI to fetch the current pair and near-term next pair.

Acceptance Criteria
Gap- and Energy-Aligned Pair Selection
Given a user has a current calendar gap [start,end], an active energy_band E, and a candidate micro-habit library with duration_sec and load_tags When the engine computes scores and selects at time T within config.determinism_window_s Then it returns exactly two distinct micro_habit_ids ordered by final_score And each pick’s duration_sec <= gap_length_sec and <= config.max_duration_sec And each pick’s load_tags are compatible with E per config.energy_to_load_map And each pick’s final_score >= config.min_selection_score And no candidate violating any hard constraint (duration, energy compatibility) appears in the output
Deterministic Selection Within Time Window
Given identical inputs and context within the same config.determinism_window_s (same gap window, energy_band, preferences, compliance history, weights, and candidate set) When the engine is invoked N >= 2 times Then the returned pair (ids and order) and selection_trace_id are identical across invocations And when the determinism window has elapsed OR any material input change occurs per config.material_change_thresholds, subsequent invocations may return a different pair
Category Diversity and Repetition Fatigue Guardrails
Given a candidate set where each item has a primary_category and the user has recent_history of executed/selected items When the engine selects the pair Then the two picks must have different primary_category values And neither pick appears in recent_history within config.repetition_cooldown_window And if no pair satisfies both rules, the engine selects the highest-scoring valid pair under a documented relaxed_rule and logs reason_code = RELAXED_DIVERSITY
Fallback Behavior for No Calendar or Low Confidence
Given calendar access is unavailable OR valid_candidate_count < 2 OR confidence_score < config.low_confidence_threshold When the engine is invoked Then it returns two picks from the configured fallback_set matched to time_of_day and default_energy_band And the response includes reason_codes indicating the fallback path and a why_this text for each pick And selection completes with p95_latency_ms <= 200 and is deterministic within config.determinism_window_s
Selection Rationale Logging and Analytics Readiness
Given a pair has been selected When the engine returns the response Then it writes a selection_event with fields: user_pseudo_id, trace_id, timestamp_utc, inputs_snapshot (gap, energy_band, preferences, compliance_features), weights_vector, top10_candidates_with_scores, chosen_pair_ids_and_scores, why_this_text_per_pick, reason_codes, experiment_variant_id, config_version And the event contains no raw PII and passes redaction unit tests And the event is available in the analytics sink within 5 minutes (p95)
Service Interface for Current and Near-Term Next Pair
Given a client calls the selection service with a valid auth token and time_context When the request is processed Then the API responds 200 with payload containing: current_pair[2], next_pair[2], why_this per pick, stable_until_ts, determinism_window_s, reason_codes, experiment_variant_id, config_version And the API returns 4xx on invalid input and 5xx with retry_classification on server errors And the API meets p95_latency_ms <= 200 and uptime SLO >= 99.9% over 30 days
Curated Micro‑Template Library (30–120s)
"As a new user, I want simple, ultra-short habit options so that getting started feels easy and doable."
Description

Maintains a vetted library of micro-habit templates constrained to 30–120 seconds with consistent schema: title, one-line instruction, duration, category, required context/equipment, energy band, and success criteria. Implements a tagging taxonomy to support matching and diversity rules. Includes authoring tools, review workflow, localization scaffolding, and versioning/rollback. Ensures accessibility-ready copy and motion alternatives. Provides lightweight telemetry on usage and outcomes per template for continuous curation.

Acceptance Criteria
Template Schema and Duration Constraints
Given an author drafts a new template, When they attempt to save, Then the system validates that title, oneLineInstruction, durationSeconds, category, requiredContextEquipment, energyBand, and successCriteria are present and non-empty. Given durationSeconds is provided, When validated, Then it must be an integer between 30 and 120 inclusive; otherwise the save is blocked and an inline error message is displayed. Given title and oneLineInstruction, When validated, Then title length is 1–60 characters and oneLineInstruction length is 1–140 characters. Given energyBand is set, When validated, Then it must be one of [low, medium, high]. Given a successful save, Then the template is assigned a unique immutable templateId.
Tagging Taxonomy and Diversity Support
Given a template is saved, When tags are validated, Then all tags must come from the controlled taxonomy and include at minimum one category tag, one context/equipment tag (or "none"), and one energyBand tag. Given the library is queried with a tag filter, When matching, Then only templates whose tags satisfy the filter are returned and all returned templates respect the 30–120 second duration constraint. Given the taxonomy is updated by an admin, When changes are published, Then existing templates with deprecated tags are flagged and cannot be republished until remapped to active tags. Given a request for a batch of N candidate templates, When diversity rules are applied, Then the API can return a tag-facet summary enabling clients to select a set with no duplicate category within N where possible.
Authoring Tools: Validation, Preview, and Draft Management
Given a user with the Author role, When creating or editing a template, Then inline validation is triggered on field blur and on save, preventing submission until all required fields pass. Given a draft template, When the author selects Preview, Then a mobile preview renders the title and oneLineInstruction exactly as they will appear in-app and shows the duration and required context/equipment badges. Given an existing template, When the author chooses Duplicate, Then a new Draft is created with a new templateId, version set to 0, and all localizable fields marked as Untranslated for non-base locales.
Review Workflow and Publication Controls
Given a Draft, When the author submits for review, Then status transitions to In Review and the content becomes read-only to the author. Given an In Review template, When a Curator requests changes with comments, Then status becomes Changes Requested and the author can edit and resubmit; comments are retained in the audit log. Given an In Review template, When a Curator approves and publishes, Then status becomes Published and the template becomes discoverable via public library queries within 5 minutes; non-Published templates never appear in public queries.
Localization and Accessibility Gates
Given a template is Published, When localization is checked, Then a complete base locale (en-US) must exist for title, oneLineInstruction, and successCriteria; otherwise publish is blocked. Given a client requests a locale that is missing for a field, When content is served, Then the system returns the base-locale value and includes translationStatus=missing for that locale in the metadata. Given a new supported locale is added, When scaffolding runs, Then placeholder localization records are created for all existing templates with status Untranslated without altering the Published version content. Given a template includes physical motion or animated guidance, When validated, Then it must include a motionAlternative that is equivalent and can be performed seated or with minimal movement. Given copy fields are analyzed pre-publish, When readability is scored, Then Flesch–Kincaid grade level is ≤ 8 and emoji-only or color-only instructions are rejected.
Versioning, Rollback, and Audit Trail
Given a Published template is edited, When changes are saved, Then a new version with an incremented version number is created; previous versions are immutable and remain addressable by templateId+version. Given a rollback is requested to version N, When confirmed by a Curator, Then a new version N+1 is created with content identical to version N and becomes the active Published version; all read APIs serve the latest within 60 seconds. Given any create, edit, approve, publish, or rollback action, When logged, Then the audit trail records actor, timestamp, action, and a diff of changed fields.
Lightweight Telemetry on Usage and Outcomes
Given a template impression, selection, or completion occurs, When telemetry is emitted, Then events include templateId, version, eventType ∈ {impression, selection, completion}, timestamp, and outcome ∈ {success, fail, cancel} for completion, and exclude PII. Given a user is offline, When telemetry is queued, Then events are stored locally and flushed within 24 hours of reconnect with exponential backoff and a max of 5 retries per event. Given telemetry is configured as lightweight, When impressions are tracked, Then at most one impression event per template per app session is sent. Given user consent is required, When consent is not granted, Then no telemetry events are sent or stored; when consent is revoked, queued events are purged.
One‑Tap Swap UX
"As a distracted worker, I want to replace a suggestion with one tap so that I can keep momentum without scrolling or configuring."
Description

Enables instant swapping of either item in the current pair with up to three curated alternatives that still satisfy gap duration, energy, and preference constraints. Provides sub-200ms response with optimistic UI, haptic feedback, and offline fallback from a pre-fetched shortlist. Prevents choice overload with clear exit/back affordances and caps swap cycles to avoid infinite hunting. Records swap reasons (implicit via selection) to improve future recommendations without extra user input. Fully accessible with screen reader support and large tap targets.

Acceptance Criteria
Instant Swap Latency and Optimistic UI
Given the user is online with a pre-fetched shortlist, When the user taps Swap on either item, Then an optimistic replacement appears within 100ms at p95 and haptic feedback triggers within 50ms. Given the optimistic replacement is shown, When the server confirms the selection, Then the final state is applied within 200ms at p95 and 300ms at p99. Given the swap interaction occurs, Then the UI remains responsive with no more than 2 dropped frames on 60Hz devices during the transition.
Curated Alternatives Meet Constraints
Given a current pair and detected gap duration, energy band, and preference flags, When the user opens the swap sheet, Then 1 to 3 alternatives are displayed and never more than 3. Given alternatives are displayed, Then each alternative’s estimated duration is between 30 and 120 seconds inclusive and does not exceed the available gap. Given alternatives are displayed, Then each alternative matches the current energy band and preference flags. Given alternatives are displayed, Then each alternative shows a visible Why this note.
Swap Cycles Cap and Clear Exit/Back
Given a single item in the pair, When the user performs consecutive swaps on that item, Then no more than 3 swaps are allowed within a 10-minute window for that item. Given the swap cap is reached, When the user attempts another swap, Then the swap control is disabled and a non-blocking prompt suggests proceeding with the current choice, with Back and Close options visible. Given the user selects Back or Close, Then the prior state is preserved (no unintended change), and the swap UI dismisses within 100ms at p95.
Offline Fallback from Pre-Fetched Shortlist
Given the device is offline, When the user taps Swap, Then the app presents 1 to 3 alternatives from the pre-fetched shortlist within 150ms at p95. Given the device is offline and no shortlist exists, When the user taps Swap, Then an offline message and Back option are shown, no error dialog is thrown, and the original item remains selected. Given the user completes a swap while offline, Then the change is applied locally and synced within 5 minutes of reconnection at p95; if sync fails, it retries with exponential backoff until success.
Implicit Swap Reason Capture and Telemetry
Given the user selects an alternative, When the swap is confirmed, Then an event is recorded containing at minimum: current_item_id, chosen_template_id, rejected_template_ids, position_index, timestamp (UTC), context_gap_seconds, energy_band, preference_flags, and network_state. Given the event is recorded, Then no additional prompts or inputs are shown to the user to capture reasons. Given the device is online, When the event is queued, Then it is persisted within 50ms and delivered to the recommendation service within 10 seconds at p95; if offline, it is queued and retried until success. Given a reconnect occurs, Then queued events are delivered in order with at-most-once application; observed event loss rate over a rolling 24h window is < 0.1%.
Accessibility and Settings-Respectful Feedback
Given a screen reader is active, When the swap sheet opens, Then focus lands on the sheet header, the title and number of options are announced, each option exposes name, role=button, and Why this as an accessible description. Given the swap UI is rendered, Then all interactive elements have minimum tap target size 44x44 points and meet WCAG 2.2 AA contrast ratios. Given keyboard navigation is used, Then all controls are reachable in logical order, and Esc/Back closes the sheet. Given system Reduced Motion or Haptics is enabled, When the swap occurs, Then animations are minimized and haptic feedback is suppressed.
“Why This” Explainability Note
"As a skeptical user, I want to see why a suggestion was chosen so that I trust it and am more likely to follow through."
Description

Displays a concise rationale below each pick showing the top factors that influenced the selection (e.g., “2‑min gap before your next meeting,” “High‑energy slot,” “You chose Focus today”). Uses privacy-safe phrasing that never reveals sensitive event details. Supports a tap for expanded context, localized copy, and an option to hide/show explanations. Provides debugging hooks for QA to view underlying weights in non-production builds. Tracks whether explanations are viewed to correlate with adherence.

Acceptance Criteria
Display concise 'Why this' note with top factors
Given a user has Prime Pair picks generated When the pick cards render Then each pick displays a "Why this" note directly below the habit title And the note lists 1–3 top factors ranked by weight And each factor label is 8–40 characters and the entire note is ≤ 140 characters And the note is visible without any additional tap
Privacy-safe phrasing for rationale
Given the app has access to calendar events with titles, attendees, and locations When generating the "Why this" note Then no exact event titles, attendee names, email addresses, phone numbers, or locations are shown And no exact timestamps are shown (use relative phrases like "2‑min gap") And the note uses only normalized factor labels from the approved copy list And automated tests confirm notes do not match patterns for emails, phone numbers, or HH:MM time formats
Tap to view expanded context
Given a "Why this" note is visible When the user taps the note Then an expanded panel appears within 300 ms showing up to 5 factors with brief descriptions And the panel includes a close control and can be dismissed by tapping outside the panel area And all phrasing remains privacy-safe per policy And screen reader focus moves to the panel header and pressing Escape closes it on desktop
Hide/Show explanations preference
Given the user toggles "Show explanations" off in Settings or via the inline control When Prime Pair picks are shown Then the "Why this" notes and their tap targets are hidden And the preference persists across app restarts for the same account And toggling the setting back on restores the notes without needing to regenerate picks And analytics record the toggle event with the new state
Localization with fallback
Given the device locale is supported by the app When the "Why this" note and expanded context are shown Then all strings are displayed in the device language with correct number and unit formatting And if a localized string is missing, an English fallback is shown without placeholder keys And pluralization rules are applied correctly (e.g., 1 min vs 2 mins) And localization keys are never visible to the user
QA-only debugging hooks for factor weights
Given a non-production (QA/Debug) build is running When the QA gesture (long-press on the "Why this" note for 2 seconds) is performed Then a debug overlay shows candidate factors with normalized weights (0.00–1.00) and selection thresholds And the overlay can copy the payload as JSON and logs it to the console And the gesture and overlay are disabled in Production builds And no debug data is sent to analytics
Track explanation views for adherence correlation
Given analytics is enabled and consent is granted When a "Why this" note is shown, expanded, or hidden Then events explanation_impression, explanation_expand, and explanation_toggle are sent with timestamp, pick_id, habit_id, locale, and build_type And events link to adherence outcomes via shared session_id and pseudonymous user_id And event payloads contain no PII or raw calendar text And event delivery success rate is ≥ 95% within 5 minutes in staging

Room Fit

Matches you with the right accountability environment—Solo, Quiet, or Social—using a quick vibe check and privacy preferences. Instantly creates or joins a starter room with preset pseudonym, avatar style, and time-blur so you feel comfortable showing up from the very first session.

Requirements

Vibe Check Onboarding
"As a remote creator, I want a quick vibe check to set my comfort and focus style so that I’m placed in a room that fits me from the first session."
Description

Lightweight, skippable onboarding that captures the user’s current focus vibe (Solo, Quiet, or Social), privacy comfort (pseudonym, avatar style, time-blur), and session timing in 3–5 taps. Persists preferences to the user profile, exposes them to the matching engine via API, and allows quick edits from the session header. Includes accessibility and localization, analytics for completion/drop-off, default-to-private guardrails when abandoned, and A/B testing hooks for question order and copy.

Acceptance Criteria
Capture vibe, privacy, and timing in ≤5 taps
Given a user without saved preferences When they begin Vibe Check onboarding Then they can complete selection of vibe (Solo, Quiet, Social), privacy options (pseudonym on/off, avatar style, time-blur on/off), and session timing in no more than 5 taps from the first screen including the final confirm And privacy defaults are preselected (pseudonym=on, time-blur=on, avatar style=auto) enabling completion in ≤3 taps if unchanged And progression is blocked until a vibe is selected And the final Start CTA is enabled only after vibe and timing are set
Skippable flow with private-by-default guardrails
Given the onboarding is visible When the user taps Skip or dismisses/abandons the flow Then a private Solo session starts with an auto-assigned pseudonym, auto-generated avatar style, and time-blur enabled And no onboarding preferences are persisted to the user profile And no social presence or room join is broadcast to other users And an analytics event VIBE_CHECK_SKIPPED or VIBE_CHECK_ABANDONED is emitted with reason=skip|dismiss
Preferences persist and are exposed via API
Given the user completes onboarding When the user profile is retrieved via API Then stored preferences include vibe, pseudonym flag, avatar style, time-blur flag, and preferred session timing And subsequent app launches prefill and bypass the onboarding using the saved values by default And the matching engine can read the same values via an exposed endpoint in the current session And updating any preference later results in the profile and matching-engine payload reflecting the change within the same session
Quick edits from session header propagate and persist
Given an active session header is visible When the user taps the Edit preferences control Then an inline panel opens showing current vibe, pseudonym toggle, avatar style, and time-blur toggle And changes to any single field can be saved in no more than 2 taps And after save, the header reflects the updates immediately and the backend profile is updated And the matching engine API reflects the updated values without requiring app restart
Starter room auto-create/join honors vibe and privacy
Given the user has selected a vibe and confirmed onboarding When the session begins Then for Solo a private local session is created and not discoverable by others And for Quiet a room is created or joined with reactions muted and presence hidden outside the room And for Social a starter room is created or joined that is visible in discovery And in all cases the selected pseudonym and avatar style are applied, and visible timestamps are time-blurred per the user’s setting
Accessibility and localization compliance for onboarding
Given a screen reader is enabled When navigating the onboarding Then all interactive elements have descriptive labels, logical focus order, and are reachable via keyboard/switch control And color contrast for text and controls meets WCAG 2.1 AA And dynamic text scaling up to 200% does not truncate or overlap actionable content And all onboarding strings are externalized and fully translated for supported locales with correct date/time formats and pluralization And right-to-left locales render with mirrored layout where applicable
Analytics funnel and A/B testing hooks are enabled
Given a user enters the onboarding When they progress through steps Then analytics events fire for view, step selections, completion, skip, and abandonment with sessionId, hashedUserId, and variantId And event delivery success rate is at least 95% within 5 minutes in staging telemetry And feature flags allow at least two variants of question order and copy, and the assigned variant is logged once per session And analytics and flag payloads exclude personally identifiable information
Room Type Matching Engine
"As a knowledge worker, I want the app to choose the best room type for me based on my preferences and context so that I can start without decision fatigue."
Description

Rule-based matching service that maps vibe and privacy inputs to Solo, Quiet, or Social rooms while factoring availability, time zone, capacity, and recent engagement. Provides deterministic, explainable routing with configurable weights and feature flags. Supports fallbacks (join existing starter room, create new room, or suggest next-best type if target unavailable), exposes REST/GraphQL endpoints, logs decisions for auditing and future ML, and rate-limits rematches within a session to prevent thrashing.

Acceptance Criteria
Deterministic, Explainable Routing
Given identical inputs (userId, vibe, privacy, availabilityWindow, timeZone) and unchanged config/system state, When the client requests a match twice within the same session, Then the engine returns the same roomType, action, and targetId. Given a routing decision is returned, When inspecting the response, Then it includes an explanation object containing considered factors, per-factor weights, candidate scores, chosen candidate, and active featureFlags. Given a routing decision is returned, When comparing two evaluations with the same inputs and config, Then the explanation payloads are identical byte-for-byte except for timestamp fields.
Rule-Based Mapping With Configurable Weights and Feature Flags
Given config weights favor Solo for privacy=High and focus vibe (e.g., ruleset v3 where SoloScore = 0.8, QuietScore = 0.5, SocialScore = 0.1), When input is vibe=Focus and privacy=High, Then the engine selects roomType=Solo. Given config weights favor Social for privacy=Low and connect vibe (e.g., SocialScore highest), When input is vibe=Connect and privacy=Low and featureFlag disableSocial=false, Then the engine selects roomType=Social and the explanation cites weights leading to highest score. Given the same inputs but featureFlag disableSocial=true, When evaluating candidates, Then Social is excluded from consideration, the next-highest scoring type is selected, and the explanation includes a flag:block:social reason.
Availability, Time Zone, Capacity, and Engagement Constraints
Given target type=Quiet has rooms A (tzDistance=1h, capacityRemaining=3, engagementScore=0.7) and B (tzDistance=4h, capacityRemaining=5, engagementScore=0.8) and config requires tzDistance<=2h and engagementScore>=0.4, When matching, Then room A is selected. Given all rooms in target type exceed configured capacity or violate tzDistance, When matching, Then no over-capacity or out-of-window room is selected and a fallback path is triggered. Given multiple candidates satisfy constraints, When matching, Then the highest total score after applying weights and penalties (tzDistance penalty, low engagement penalty) is selected and included in explanation.candidateScores.
Fallback Sequencing and Actions
Given target type is unavailable due to constraints, When there exists an existing starter room of that type with capacity within tz window, Then action=join and response includes roomId, roomType, and explanation.reason=fallback:join-starter. Given no existing starter room for target type, When create is permitted by config, Then action=create and response includes createParams {roomType, starter=true} and explanation.reason=fallback:create-starter. Given neither join nor create is available for target type, When evaluating next-best types, Then action=suggest and response includes suggestion.roomType with rationale and explanation.reason=fallback:suggest-next-best.
Decision Logging for Audit and Future ML
Given a match request is processed, When the decision is produced, Then a structured log/event is emitted containing decisionId, timestamp, sessionId, userRef (hashed), inputs, configVersion, featureFlags, rankedCandidates with scores, chosenCandidate, action, and latencyMs. Given decisionId from the response, When querying the logging sink by decisionId, Then exactly one matching record exists and fields match the response payload (excluding redactable PII and timestamps). Given a validation error occurs (HTTP 422), When logging, Then an error event is emitted with decisionId, input validation errors, and no candidate scores.
REST and GraphQL API Contract
Given POST /v1/match with body {userId, sessionId, vibe, privacy, availabilityWindow, timeZone} and optional idempotencyKey, When inputs are valid, Then respond 200 with {decisionId, action, roomType, roomId?, createParams?, suggestion?, explanation} matching the OpenAPI schema. Given the GraphQL mutation matchRoom(input), When inputs are valid, Then respond 200 with a payload equivalent in fields and types to the REST response as defined in the GraphQL schema. Given missing or invalid fields, When requesting via REST or GraphQL, Then respond 422 with a machine-readable error {code, field, message} and no decision side effects. Given duplicate requests with the same idempotencyKey within 10 minutes, When processed, Then the same decision is returned (same decisionId and payload) with Idempotent-Replay header=true.
In-Session Rematch Rate Limiting
Given config maxRematchesPerSession=2 and windowSeconds=600, When a client performs more than 2 rematch requests within the same session and window, Then the engine returns HTTP 429 (or GraphQL error with code=RATE_LIMITED), includes Retry-After seconds, and no new decisionId is generated. Given a rematch is denied due to rate limiting, When checking logs, Then an event is recorded with reason=rate_limited, currentCount, limit, windowSeconds, and sessionId. Given the window elapses, When the client requests another rematch, Then the counter resets and a new decision is evaluated and returned.
Instant Starter Room Join/Creation
"As a user, I want to enter a suitable room instantly so that I can begin my session without setup delays."
Description

One-tap flow that immediately joins an available starter room or provisions a new one within a 2-second target SLA. Pre-applies room presets (pseudonym, avatar style, time-blur) and configures communication mode per type (Solo: self check-in, Quiet: text-only reactions, Social: lightweight reactions). Handles capacity and region selection, provides optimistic UI with graceful offline fallback, includes retries and queueing when rooms are full, and emits telemetry for time-to-join and error rates.

Acceptance Criteria
One-Tap Join Meets 2s SLA
Given the user is on the join screen and taps "Start Now" with packet loss < 2% and RTT ≤ 150 ms and service health is green, When an eligible starter room is available, Then end-to-end time from tap to "Joined" state is ≤ 2,000 ms in ≥ 95% of attempts and ≤ 2,500 ms in ≥ 99% of attempts. Given 500 join attempts under the above conditions, When measured, Then the median time-to-join is ≤ 1,200 ms.
Optimistic UI and Offline Fallback
Given the user taps "Start Now", When the request is dispatched, Then a "Joining..." optimistic state renders within 100 ms and remains responsive until success or failure. Given the device is offline at tap, When the user taps "Start Now", Then an Offline Solo Session starts immediately, a local check-in is recorded, and the join is queued with exponential backoff (1s, 2s, 4s; max 3 retries). Given connectivity is restored within 30 minutes, When any queued attempt succeeds, Then the offline session is reconciled to the joined room without duplicate check-ins.
Auto-Provision Starter Room When None Available
Given no starter room of the selected type exists with available capacity in the permitted region(s), When the user taps "Start Now", Then a new starter room is created and the user is placed as the first member within 2,000 ms. Given the room is created, When inspecting its properties, Then capacity, visibility, and presets match the configured defaults for that room type.
Presets Applied on Join (Pseudonym, Avatar Style, Time-Blur)
Given a user joins or creates a starter room, When the session initializes, Then a pseudonym is auto-assigned matching the pattern AdjectiveAnimal### and is unique within the room and not equal to the user's handle. Given the user’s privacy preferences, When the avatar renders, Then the configured avatar style is applied and persists across rejoin within the same session. Given time-blur is enabled, When timestamps are displayed to other members, Then start time is obfuscated by ± up to 5 minutes (or rounded per setting), and precise timestamps are not exposed in room UI or member lists.
Room Type Communication Modes Enforced (Solo/Quiet/Social)
Given a Solo room, When in session, Then only self check-in controls are available and reaction/chat controls are not rendered; other users cannot join. Given a Quiet room, When in session, Then text-only reactions are enabled and voice/video/mic controls are absent; attempting to invoke disallowed controls shows an explanatory tooltip. Given a Social room, When in session, Then lightweight reactions (emoji, clap) are enabled and long-form text chat and audio/video are not available; reaction rate is limited to ≤ 5 per second per user.
Capacity, Retry, and Region Selection Logic
Given the preferred region and room type, When the first target room is full, Then the system retries up to 2 other eligible rooms in the preferred region, then the lowest-latency allowed region, before queueing. Given the user is queued, When a slot opens, Then the user auto-joins within 1 second and the UI updates from "In queue (position N)" to "Joined" without additional taps; canceling the queue removes the user immediately. Given rooms have configured capacities per type, When a new room is created, Then the capacity matches the configured default and is enforced on join attempts. Given the user has an explicit region preference or residency constraint, When joining or creating, Then the selected region honors the preference or constraint and optimizes for lowest measured RTT among allowed regions.
Telemetry for Join and Error Rates
Given any join or create attempt, When it starts, Then a room_join_attempt event is emitted with fields: session_id, user_anonymous_id, room_type, intended_region, timestamp_start. Given a join or create attempt completes, When successful, Then a room_join_success event is emitted with fields: session_id, room_id, region, time_to_join_ms, path (joined|created|queued), timestamp_end. Given a join or create attempt fails, When it fails, Then a room_join_fail event is emitted with fields: session_id, error_code, region, retry_count, timestamp_end. Given telemetry ingestion, When observed in analytics, Then 95% of events are queryable within 5 minutes and PII (real name, email, IP) is not included in payloads.
Privacy & Pseudonym Presets
"As a privacy-conscious user, I want pseudonyms and blurred time by default so that I feel safe participating."
Description

Automatic assignment and management of pseudonymous identity and avatar style per room type with privacy-first defaults. Prevents exposure of real name or profile photo by default, applies time-blur ranges, and surfaces visibility controls in the session header. Supports per-user overrides with persistent storage, enforces consistent presentation across feeds and reactions, minimizes data collection, secures true timestamps server-side, and includes region-specific compliance hooks.

Acceptance Criteria
Default pseudonym and avatar on first room entry
Given a signed-in user with a real name and profile photo on their account And the user has no prior privacy override for the target room type When the user is matched to or joins a room of type Solo, Quiet, or Social Then the client renders a system-assigned pseudonym and preset avatar style for that room type in the session header, participant list, and reaction composer And the user's real name and profile photo are not rendered in any client UI surface for that room And the assigned pseudonym and avatar remain stable for the duration of the session
Time-blur ranges applied; true timestamps secured server-side
Given time-blur defaults are enabled for the room type When the user posts a check-in, reaction, or completion event Then the client displays a time-blur bucket (e.g., 5–10 min) instead of an exact timestamp And the API responses for room events do not include exact timestamps, only the defined blur bucket or relative time And the server stores the exact event timestamp in a secure table accessible only via privileged service accounts And direct client endpoints never return exact timestamps
Visibility controls surfaced in session header
Given the user is in an active room session When the user opens the session header controls Then controls to manage pseudonym, avatar style, and time-blur visibility are present and labeled And the default states are pseudonym ON, preset avatar ON, and time-blur ON And toggling a control updates the current session UI within 1 second without exposing real name or profile photo unless explicitly opted in And the chosen settings are saved as the user's overrides for that room type
Per-user privacy overrides persist across devices
Given the user changes their pseudonym, avatar style, or time-blur setting for a specific room type When the user starts a new session of the same room type on another device Then the previously chosen overrides are applied within 60 seconds of session start And the overrides do not retroactively reveal real names or exact timestamps on past events And in case of concurrent edits, the latest change timestamp wins
Consistent pseudonymous presentation across surfaces
Given the user participates in a room session When their identity is rendered on any surface (room roster, live reactions stream, activity feed cards, profile hover, and push notifications) Then the same pseudonym and preset avatar style are shown And the real name and profile photo are not displayed on any surface for that room And the time-blur bucket is consistently used wherever event timing is shown
Data minimization for events and analytics
Given events are written to persistence and telemetry is emitted When inspecting stored room event records and analytics payloads Then no fields contain the user's real name or profile photo reference And only pseudonymous user ID, avatar style key, and blur bucket are stored for client-consumable records And analytics payloads omit exact timestamps and PII, using aggregated or bucketed values And a schema migration or audit query verifies these constraints
Region-specific compliance hooks enforce stricter defaults
Given the system detects the user's region (e.g., EU, US) at session start When initializing privacy presets for the room Then a compliance hook is invoked with user region, room type, and intended data fields And if the hook returns stricter policies (e.g., larger blur bucket, disable opt-in to real name), those are applied before any UI is rendered And if the hook is unavailable or errors, the system falls back to the strictest default policies
Time-Blur Display Consistency
"As a user, I want my activity times to appear as ranges everywhere so that my schedule isn’t exposed."
Description

Unified rendering layer that shows check-in and session times as blurred ranges across rooms, feeds, profiles, and notifications while retaining precise timestamps server-side for analytics and streak logic. Inherits defaults from privacy presets, supports per-room overrides, and ensures shared screenshots and deep links honor blur settings. Includes locale-aware formatting, edge-case handling for day boundaries, and automated tests.

Acceptance Criteria
Cross-Surface Blurred Time Rendering Consistency
Given Time-Blur is enabled at 15 minutes and a user checks in at 14:03 local time When viewing the same item on Room timeline, Feed card, Profile streak view, and Notifications list Then the time renders as the identical string "14:00–14:15" across all surfaces without exposing exact minutes or seconds And long-press, tooltips, and accessibility labels do not reveal exact timestamps And automated unit, snapshot, and E2E tests verify identical rendering across surfaces
Server Precision, Client Blur Integrity
Given a check-in recorded at 07:58:12Z When streak calculation and analytics aggregation run server-side Then they use the precise timestamp and produce correct day attribution and metrics And analytics export surfaces the full ISO timestamp When the same event is rendered in any user-facing UI, share image, or public preview Then no exact timestamp is displayed or embedded in DOM attributes, accessibility labels, or data-* fields And automated tests assert no precise times appear in rendered HTML/JSON for user-facing endpoints
Privacy Preset Default Inheritance
Given Room Fit assigns the Quiet preset with a 30-minute default blur When a new room is created via Room Fit Then the room time-blur setting defaults to 30 minutes and all room surfaces render using that blur When the user switches their privacy preset and creates another room Then the new room inherits the new preset’s default blur while existing rooms remain unchanged And automated tests cover preset-to-room inheritance and non-regression for existing rooms
Per-Room Override Persistence and Scope
Given a room currently using a 30-minute default blur When the room admin sets a per-room override to 5 minutes Then all participants immediately see 5-minute ranges in that room only, including history and new events And other rooms and global surfaces keep their own blur settings When the user reloads or switches devices Then the 5-minute override persists and re-applies And automated tests validate override persistence, scoping, and real-time re-render
Shares, Screenshots, and Deep Links Honor Blur
Given a user shares a check-in using the app’s Share action When a screenshot or share image is generated Then times appear as blurred ranges per the current room/user blur level and OCR does not reveal exact minutes/seconds When a recipient opens a deep link, OG/Twitter card, or public web view Then times display using the source room’s blur setting and URL parameters cannot disable blur And automated tests verify share images and public previews honor blur settings
Locale, Timezone, and 12/24h Formatting
Given a viewer with locale en-US and 12-hour preference When viewing a 14:03 event with a 15-minute blur Then the range renders as "2:00–2:15 PM" Given a viewer with locale de-DE and 24-hour preference Then the same event renders as "14:00–14:15" When the viewer is in a different timezone than the event creator Then ranges are shown in the viewer’s local timezone with correct DST adjustments And automated tests cover locale, 12/24h, and timezone variations
Day-Boundary and DST Edge Cases
Given a check-in at 23:58 with a 15-minute blur When rendered Then it displays as "23:45–00:00" while streak attribution remains on the check-in’s server-defined calendar day Given DST forward (missing hour) or backward (repeated hour) transitions When rendering ranges overlapping the transition Then the formatted range uses correct local offsets without invalid times and streak logic remains correct using precise server timestamps And automated tests simulate DST transitions and verify formatting and streak integrity
Refit Feedback & Rematch
"As a user, I want to quickly say whether the room fit worked and switch if needed so that future sessions match me better."
Description

Post-session micro-feedback (single tap or swipe) to capture perceived fit and comfort, with an optional quick switch to another room type. Feeds signals back into the matching engine to adjust weights for subsequent sessions. Includes undo, prompt rate-limiting to prevent fatigue, and outcome tracking (retention, streak continuation) to validate and iterate on matching efficacy.

Acceptance Criteria
Post-Session Single-Tap Fit Feedback
Given a user completes a room session and the end-of-session view is displayed When the user taps a fit option (Good, Neutral, Poor) Then the app records a feedback event containing session_id, user_pid, room_id, room_type, feedback_value in {"good","neutral","poor"}, source="single_tap", created_at (UTC) And a confirmation toast "Feedback saved" appears within 500 ms and auto-dismisses within 2 s And no additional feedback prompt is shown again for that session
Swipe Gesture Feedback and Undo
Given the feedback sheet is visible post-session When the user swipes right, then feedback_value="good"; when swipes left, feedback_value="poor"; when swipes down, the sheet dismisses with no feedback recorded Then a feedback event is recorded with the mapped value and source="swipe" And an Undo action is displayed for 5 s When the user taps Undo within 5 s Then the original feedback event is marked retracted=true and excluded from downstream matching updates, and the UI returns to the pre-feedback state When the 5 s window expires Then the feedback event becomes final
Optional Quick Rematch to Another Room Type
Given the user has submitted a "poor" fit or taps "Try a different room" on the post-session view When the user selects Solo, Quiet, or Social Then a starter room of the selected type is created or joined within 3 s at P95 And the user's pseudonym, avatar style, and time-blur preferences are applied in the new room And the user's streak is preserved with no decrement And a rematch_selected telemetry event is emitted containing prior_room_type, new_room_type, join_latency_ms, and session_id
Prompt Rate Limiting to Prevent Fatigue
Given feedback prompt rate limits are configured as max_prompts_per_day and max_prompts_per_7_days for a user When the user completes multiple sessions within a day or a 7-day window Then the feedback prompt is shown no more than the configured limits And selecting "Don't ask for 7 days" suppresses prompts for 7 days across all devices for that user And the rate-limit state persists across app restarts and device changes And a feature flag "feedback_prompt_unlimited" bypasses limits for test accounts
Matching Engine Weight Adjustment from Feedback
Given a feedback event is ingested for a user When the matching engine computes the next session recommendation Then the user's room_type preference weights are updated according to a configurable mapping within 10 minutes of event ingestion And an audit log captures before_weight, after_weight, mapping_version, feedback_event_id, and timestamp And the next recommendation call reflects the updated weights (verifiable via a debug endpoint)
Outcome Tracking for Retention and Streak Continuation
Given feedback and rematch events are produced When analytics aggregation runs (streaming and daily batch) Then metrics are computed: D1/D7/D28 retention by feedback_value, streak continuation within 24 h of session end, and rematch conversion rate And dashboards expose these metrics by room_type and cohort date And data quality checks pass with event loss < 1% and processing latency P95 < 5 min (stream) and < 2 h (batch) And alerts trigger within 5 min if thresholds are breached

First Win

Guides you into your first live check-in within 60 seconds. A lightweight overlay deep-links to your room, cues a one-tap check-in, and confirms with a subtle haptic and undo safety net. You leave with a Day‑1 streak card and a scheduled next check-in, cementing early momentum.

Requirements

60-Second First Win Overlay
"As a new user, I want a guided path to my first live check-in within a minute so that I can experience value immediately and understand how StreakShare works."
Description

A lightweight, interruptible overlay that launches on first open and guides new users from app start to their first live check-in within 60 seconds. The overlay suppresses non-essential UI, displays a clear primary CTA, and shows a subtle progress indicator to set expectations. It pre-checks critical permissions (notifications/haptics), warms navigation, and preloads the target room context to ensure instant transition. The flow is resilient: it resumes if the app is backgrounded, degrades gracefully offline, and retries on transient failures. It fires analytics for each step (view, tap, success, abandon) to measure time-to-first-check-in and funnel drop-offs. Triggering is idempotent and one-time per user until completion, with feature flag control for gradual rollout and A/B variants. Accessibility (screen reader labels, focus order, color contrast) and dark mode are first-class. Integrates with navigation, analytics, and experimentation services.

Acceptance Criteria
Happy Path: First Check-In in 60 Seconds
Given a first-time user opens the app with network connectivity and the First Win Overlay feature flag is ON And the target room context has been preloaded and navigation warmed When the overlay displays its primary CTA and the user taps it Then the app deep-links to the target room and renders the live check-in UI within 500 ms of the tap (P50) and within 1,000 ms (P90) And the user completes a one-tap check-in successfully And a subtle haptic confirmation is emitted on success if haptics are available and enabled And if notification or haptic permission is undetermined, a non-blocking prompt is shown and the user can proceed either way without exceeding the 60-second SLA And the measured time from app foreground to check-in success is less than or equal to 60 seconds
Overlay UI Suppression and Progress Indicator
Given the overlay is visible on first launch Then non-essential app UI (navigation, settings, secondary actions) is visually suppressed and non-interactive And exactly one primary CTA is focusable and labeled with the next step And a progress indicator shows the current step and remaining steps (e.g., Step 1 of 2) And the overlay can be dismissed via a close control that saves state And re-opening the app or overlay restores to the last saved step
Accessibility and Dark Mode Compliance
Given a screen reader is enabled When the overlay appears Then all actionable elements have descriptive accessibility labels and roles/traits And focus order follows visual order without skips or traps And the primary CTA is reachable via keyboard/remote input And text contrast ratio is at least 4.5:1 and non-text UI at least 3:1 in both light and dark modes And the overlay respects the system theme (light/dark) without clipped content or illegible text
Background Resume and Session Continuity
Given the overlay flow is in progress When the app is backgrounded and later resumed within 30 minutes Then the flow resumes at the exact prior step with preserved progress and inputs And timers used to calculate the 60-second SLA exclude time spent in the background And any in-flight network operations are retried or resumed without user data loss
Offline Degradation and Transient Failure Handling
Given the device is offline when the overlay starts or during the flow Then the overlay displays an offline notice and disables network-dependent CTAs or offers a Try again action And preloading attempts are retried with exponential backoff up to 3 times with a non-blocking failure message on repeated failure And if a check-in is initiated offline, it is queued locally with a visible Pending state and auto-retried on reconnect within 60 seconds, yielding haptic confirmation on success And the user can cancel a pending check-in to prevent auto-posting
Idempotent Triggering and Rollout Controls
Given the user has not completed their first check-in When they launch the app while the feature flag is ON Then the overlay triggers exactly once per session and re-triggers on subsequent launches until completion And after the first successful check-in, the overlay never triggers again for that user And experiment variant assignment is sticky per user for at least 30 days and persists across reinstalls And a remote kill switch can disable the overlay immediately without an app update
Analytics and Funnel Measurement
Given analytics are available When the user progresses through the overlay Then the following events are emitted with timestamp, user_id, session_id, variant, and step metadata: first_win_view, first_win_cta_tap, first_win_deeplink_success, first_win_checkin_success, first_win_dismiss, first_win_abandon And time_to_first_checkin is computable from events with at least 95% of sessions reporting And events are deduplicated via event_id and delivered within 60 seconds for at least 99% of sessions under normal connectivity And analytics and experimentation exposure events fire before rendering any variant-specific UI
Smart Room Resolution & Deep-Link
"As a user, I want the app to take me directly to the most relevant room so that I can start my first check-in without hunting through navigation."
Description

Deterministically resolves and deep-links the user to the most relevant room for the first check-in. The resolver prioritizes: (1) an active or scheduled room owned by the user within the next 24 hours, (2) the most recently active room, or (3) creates a private default room (e.g., "My Focus") if none exist. It validates membership/permissions, handles private rooms and invites, and pre-joins real-time channels (SSE/WebSocket) to minimize perceived latency. It supports universal links and push-notification deep links, and returns structured reasons for analytics (e.g., resolved_by=upcoming, recent, created_default). Errors and edge cases (revoked access, deleted room) are caught with user-friendly fallbacks. The API is idempotent, cached briefly to reduce server load, and instrumented for resolution time and success rate.

Acceptance Criteria
Resolve Upcoming Owned Room (<=24h)
Given the user owns at least one room that is currently active or scheduled to start within the next 24 hours When the resolver is invoked via the First Win universal link Then the resolver deterministically selects the room by priority: active owned room first; if multiple, the one with most recent activity; otherwise the owned scheduled room with the earliest start time (ties broken by most recent activity) And returns HTTP 200 with payload including room_id, deep_link, resolved_by=upcoming, and reason_detail And validates membership/ownership before returning; if validation fails, the candidate is skipped and the next eligible room is evaluated And the deep_link includes source=first_win And the client pre-joins the room’s SSE/WebSocket channels before navigation; the connection readyState is open within 1000 ms of the room screen mounting
Fallback to Most Recently Active Room
Given the user has no owned rooms active or starting within the next 24 hours but is a member of one or more rooms When the resolver is invoked Then the resolver selects the room with the highest last_active_at where the user still has access And returns payload with room_id, deep_link, resolved_by=recent And pre-joins the selected room’s SSE/WebSocket channels before navigation; the connection readyState is open within 1000 ms of the room screen mounting And if access to the most recent room is revoked, the resolver skips it and evaluates the next eligible room
Create Private Default Room When None Exist
Given the user has no accessible rooms (owned or member) When the resolver is invoked Then the service creates a private default room named "My Focus" (localized if applicable) with the user as owner, visibility=private, and zero members beyond the owner And returns payload with room_id, deep_link, resolved_by=created_default And if a default room already exists for the user, no additional room is created and that existing room_id is returned And the client pre-joins the new room’s SSE/WebSocket channels before navigation; the connection readyState is open within 1000 ms of the room screen mounting
Universal and Push Deep-Link Routing
Given a universal link or push-notification deep link targets the resolver When the link is opened on iOS or Android Then the app routes to /rooms/{room_id} for the resolved room and preserves query parameters (e.g., source=first_win, utm_*) And if the direct target room is deleted or the user’s access is revoked, a user-friendly notice is shown and the resolver falls back to the default resolution path (recent or created_default) And the deep link opens the resolved room without intermediate error screens, with a single navigation transition
Private Room Invite Handling
Given a deep link includes a valid invite token for a private room When the resolver is invoked Then the invite token is validated, the user is joined to the room, and the app deep-links to that room And the analytics payload includes invite_accept=true alongside resolved_by appropriate to the room state And if the invite token is invalid or expired, a user-friendly notice is shown and the resolver falls back to the default resolution path without joining the room; analytics includes error_code=invite_invalid
Idempotency, Caching, and Concurrency Safety
Given repeated identical resolver calls for the same user context within the cache TTL and up to 10 concurrent requests When the resolver is invoked Then all responses return the same room_id and resolved_by value And at most one default room is created in the database (no duplicates) under concurrency And the response includes Cache-Control: private, max-age=30 and an entity validator (ETag or equivalent); a subsequent conditional request returns 304 Not Modified when appropriate And the resolver is idempotent across retries (e.g., network retries or push re-delivery)
Instrumentation, Structured Reasons, and SLAs
Given the resolver processes a request When the resolution completes (success or fallback) Then an analytics event (resolver_result) is emitted exactly once with fields: user_id, room_id (nullable on failure), resolved_by in {upcoming, recent, created_default}, transport_source in {universal_link, push_link}, resolution_time_ms, success boolean, and error_code (nullable) And in a controlled load test of 1000 requests: P95 resolution_time_ms <= 400 and P99 <= 800; success rate >= 99%; analytics event loss <= 0.5% And operational dashboards display resolution_time_ms and success rate by transport_source with 1-minute granularity
One‑Tap Live Check‑In
"As a user, I want to check in with a single tap so that I can make a commitment without any setup or delay."
Description

Provides a single, prominent CTA that posts a live check-in to the resolved room with minimal friction. On tap, the client performs optimistic UI updates, emits a presence event to the room, and records a check-in with an idempotency key to prevent duplicates. It shows immediate visual acknowledgement and is tolerant of poor connectivity by queuing the action for retry and marking it as pending until server confirmation. On success, it updates the user’s streak state, surfaces inline confirmation, and notifies room participants in real time. The API enforces validation (room, user eligibility), rate limits accidental double taps, and logs telemetry (latency, success/failure, retries) for reliability SLOs.

Acceptance Criteria
Prominent CTA Visibility and Eligibility
Given an authenticated, eligible user is viewing a resolved room When the room view renders Then a single, primary "Check In" CTA is visible above the fold and is the only primary action And the CTA is enabled for eligible users and disabled with an explanatory state for ineligible users (e.g., not a member, cooldown, already checked in) And the CTA label reflects the current state: "Check In" (default), "Pending…" (in-flight), or "Retry" (after failure)
Optimistic UI Acknowledgement and Pending State
Given an eligible user viewing the room When the user taps the "Check In" CTA Then the UI acknowledges within 150 ms (tap-to-visual) by transitioning the CTA to Pending and inserting a pending check-in entry at the top of the feed And a presence event is emitted to the room immediately indicating the user is checking in And the pending check-in shows a spinner/state until server confirmation or failure And the CTA remains disabled while a check-in is pending
Idempotent Single Check-In on Rapid Taps
Given the client generates a unique idempotency key per check-in attempt When the user taps the CTA multiple times within 2 seconds Then only one check-in is created server-side And duplicate requests with the same idempotency key within 24 hours return the original check-in without creating a new record And the UI displays a single check-in entry without duplication And server-side rate limiting rejects more than 1 check-in attempt per user per room per 2 seconds with HTTP 429
Connectivity Tolerance, Queue, and Retry
Given the device is offline or experiences intermittent connectivity at tap time When the user taps the CTA Then the request is enqueued locally with the idempotency key and marked Pending in the UI And the client retries with exponential backoff up to 6 attempts or 5 minutes total, resuming immediately on connectivity restoration And while pending, the user sees a non-blocking inline pending indicator and can navigate away without losing the queued action And if all retries fail, the pending entry transitions to Failed with a retry affordance that reuses the original idempotency key
Server Confirmation, Streak Update, and Real-Time Notifications
Given the server validates and confirms the check-in When confirmation is received by the client Then the pending entry resolves to Confirmed with server timestamp, and the user's streak count updates per streak rules without double increments on idempotent replays And all room participants receive a real-time check-in event and presence update within 3 seconds p95 of server confirmation And the interim presence state is cleared or replaced with the confirmed state
API Validation and Error Handling
Given a request with an invalid room, ineligible user, or missing/invalid authentication When the API receives the check-in request Then it responds with the appropriate 4xx (404 for missing room, 403 for ineligible, 401 for unauthenticated) without creating a check-in And the client surfaces an inline error state and restores the CTA to enabled with a clear, accessible message And no presence or notification is persisted for rejected requests
Telemetry and Reliability SLO Logging
Given the check-in flow executes (success, retry, or failure) When the client and server handle the attempt Then telemetry records at minimum: idempotency key, tap timestamp, optimistic-ack latency, time-to-confirmation, retry count, outcome, and HTTP status And events are correlated via a trace ID across client and server logs And daily dashboards report p50/p95/p99 for tap-to-optimistic-ack and tap-to-confirmation, plus success rate, to evaluate SLOs
Haptic Confirm + Undo Safety Net
"As a cautious user, I want tactile confirmation and an undo option so that I can quickly correct a mistaken check-in without damaging my streak."
Description

Adds a subtle haptic confirmation on successful check-in and presents a non-blocking snackbar with an Undo action for a short grace period (e.g., 10 seconds). Choosing Undo reverts the optimistic UI, cancels the presence event, and rolls back streak updates using a reversible server-side mutation with audit logging. Haptics respect device settings and provide accessible alternatives (auditory/visual cues) as needed. Edge cases like network loss during undo are handled with clear messaging and eventual consistency. All actions are instrumented to track mis-taps prevented and user confidence signals.

Acceptance Criteria
Subtle Haptic Confirm on Successful Check-In
Given the user successfully completes a check-in When the UI transitions the check-in state to "checked" Then a single subtle haptic feedback (light intensity) is triggered within 150 ms of the state change And no haptic is emitted if the OS-level haptics setting is disabled or unsupported on the device And no haptic is triggered on failed check-ins or on Undo actions And the haptic fires exactly once per successful check-in, ignoring retries or screen redraws
Non-Blocking Snackbar With 10s Undo
Given a successful check-in When the confirmation is displayed Then a non-blocking snackbar appears within 200 ms showing descriptive copy and a prominent "Undo" action And the snackbar does not block other room interactions (taps, scrolls, reactions) And only one snackbar is active per check-in And the snackbar auto-dismisses after 10 seconds ±0.5 s if "Undo" is not tapped And tapping "Undo" dismisses the snackbar immediately and initiates reversal
Undo Reverts Optimistic UI and Server State
Given an optimistic check-in is visible When the user taps "Undo" within the grace window Then the UI reverts to pre-check-in state within 300 ms, including check-in button, streak card, and presence indicator And a reversible server mutation is sent to cancel the presence event and roll back streak updates using an idempotency key tied to the original check-in And the server records an audit log entry with checkInId, userId, roomId, previous_state, new_state, and timestamp And the client reflects the authoritative server state within 3 seconds after mutation success
Undo Under Network Loss Uses Eventual Consistency
Given network connectivity is lost or unstable when "Undo" is tapped When the Undo action is initiated Then the UI immediately reverts locally and shows a "Undo pending" indicator with clear messaging And the client queues a reverse mutation with an idempotency key and retries using exponential backoff (initial 1 s, max 30 s) for up to 5 attempts or 2 minutes, whichever comes first And upon successful retry, the UI updates to "Undo confirmed" and clears the pending indicator And if retries are exhausted, a non-intrusive banner shows "Sync needed" with a Retry control, and the client reconciles by fetching authoritative state on next connectivity and resolving conflicts within 3 seconds
Accessible Alternatives When Haptics Unavailable
Given the device lacks haptics or OS haptics is disabled (or Reduce Motion is enabled) When a check-in succeeds Then an accessible visual confirmation is shown within 150 ms (reduced-motion variant if Reduce Motion is on) and an auditory cue plays if system sound is enabled And screen readers announce "Check-in confirmed" within 1 second and "Check-in undone" upon successful undo And confirmation visuals and snackbar text meet WCAG 2.1 AA contrast And all actions remain fully operable via keyboard/switch access and are focus-visible
Analytics and Audit for Haptic/Undo Flow
Given user interactions with check-in and undo When these events occur Then analytics events are emitted: check_in_success, snackbar_shown, haptic_fired (when applicable), undo_shown, undo_clicked, undo_success, undo_failed; each includes userId, roomId, checkInId, timestamp_ms, device_haptics_enabled, and correlation/idempotency keys And undo_success records latency_ms for (check-in→undo) and (undo tap→server confirm) And a derived metric mis_taps_prevented increments when undo_success occurs within 3 s of check-in and no re-check-in occurs within 30 s And server audit logs capture reversal details (before/after states, actor, reason="user_undo") independent of client analytics opt-in
Day‑1 Streak Card
"As a motivated user, I want to see my Day‑1 streak card after checking in so that I feel rewarded and encouraged to return tomorrow."
Description

Generates and displays a celebratory Day‑1 streak card immediately after the first successful check-in. The card highlights streak count, room name, check-in time, and a concise motivational message, with visuals optimized for quick recognition and sharing to the in-app feed. It persists to the user profile and room history, supports dark mode and accessibility text, and loads instantly via pre-fetched assets. The card is dismissible, does not block the flow, and records impressions, shares, and dismiss reasons to assess motivational impact. Localization and dynamic theming are supported via configuration.

Acceptance Criteria
Immediate Day‑1 Card Display After First Check-In
Given a user completes their first successful check-in in room R and the server acknowledges success When the client receives the confirmation Then the Day‑1 streak card is presented within 500 ms And the card uses pre-fetched assets (no additional network requests > 50 KB before display) And the card is not shown for any check-in that is not the user's first in room R And the card provides a visible close control and supports standard platform swipe-to-dismiss And the card does not block navigation or input to other UI elements
Day‑1 Card Content Accuracy and Formatting
Given the first check-in occurred at timestamp T in time zone Z for room R When the Day‑1 card is displayed Then it shows streak count equal to 1 And it displays the room name R And it displays the check-in time formatted in the user's locale and local time zone Z And it includes a motivational message from the configured Day‑1 catalog for the user's locale/theme And no text is truncated or clipped on devices with width >= 320 pt at default font size
Persistence to Profile and Room History
Given the Day‑1 card has been generated When the user views their profile Then a Day‑1 streak entry is present with the same content and timestamp And when the user views the room R history Then the same Day‑1 entry is present And the entry persists across app relaunch and device restart And repeating the first-check-in flow does not create duplicate Day‑1 entries (idempotent by user_id+room_id)
In-App Feed Sharing from Day‑1 Card
Given the Day‑1 card is visible When the user taps Share to Feed and confirms Then a post is created in the in-app room feed within 2 seconds with a faithful rendering of the card And the share count metric for the card increments by 1 And if the user cancels the share, no post is created and no share count is recorded
Accessibility and Dark Mode Compliance
Given the device is set to dark mode When the Day‑1 card is displayed Then all card text meets a contrast ratio of at least 4.5:1 against background And given a screen reader is active When the card appears Then the card announces "Day‑1 streak card" followed by streak count, room name, check-in time, and message And all actionable elements have a minimum hit target of 44x44 points and are reachable via keyboard/remote focus where applicable And with dynamic type up to 200%, content reflows without overlap, clipping, or loss of functionality
Analytics Events for Motivation Assessment
Given the Day‑1 card becomes fully visible for at least 500 ms When tracking is enabled Then exactly one impression event is recorded per display with fields: user_id_hash, room_id, card_id, timestamp, locale, theme, variant And when the user dismisses the card, a dismiss event is recorded with a reason from [tap_close, swipe_dismiss, timeout] And when the user shares from the card, a share event is recorded with destination=in_app_feed And events are queued and retried until a 2xx response is received, without recording duplicate events
Localization and Dynamic Theming via Configuration
Given the user's locale is L When the Day‑1 card is displayed Then all strings are sourced from the localization catalog for L, with fallback to en-US when a key is missing And dates/times and numerals follow L formatting rules And theme colors, typography, and iconography are resolved from configuration tokens without hardcoded values And switching the app theme at runtime reapplies styles to the visible card within 100 ms without visible flicker
Auto‑Schedule Next Check‑In
"As a busy user, I want the app to schedule my next check-in for me so that I can maintain momentum without extra planning."
Description

Prompts the user to set the next check-in immediately after the first one, offering smart defaults such as "Same time tomorrow" and context-aware recommendations based on timezone, typical work hours, and room cadence. On selection, it creates a scheduled check-in, registers a reminder notification, and optionally offers calendar integration (add-to-calendar intent) with permission gating. The UI is one-tap, non-intrusive, and skippable, with clear confirmation and the ability to edit later. Handles DST/timezone changes, quiet hours, and duplicate reminders. Telemetry captures opt-in rate, reminder delivery, and next-day conversion to validate effectiveness.

Acceptance Criteria
One‑Tap Next Check‑In Default Selection
- Given I complete my first live check-in, When the auto-schedule prompt appears, Then "Same time tomorrow" is offered as the primary option in my current timezone. - Given I tap the primary option, When the action is processed, Then a next check-in is scheduled within 500ms and a confirmation message with subtle haptic feedback is shown. - Given a scheduled check-in is created, When I tap "Undo" within 10 seconds, Then the schedule and any associated reminders are removed and the confirmation updates to "Scheduling undone". - Given a scheduled check-in is created, When I open the Day‑1 streak card or Schedule screen, Then I can view and edit the scheduled time.
Context‑Aware Recommendations
- Given my profile timezone, declared work hours, and room cadence are known, When the prompt renders, Then it shows 2–3 options including "Same time tomorrow" and a context-aware recommended slot within my work hours and room cadence. - Given quiet hours are configured, When generating options, Then no option falls inside quiet hours. - Given my locale uses 12/24h format, When times are displayed, Then they follow locale formatting. - Given I select any option, When confirmed, Then the selection is persisted and attributed as "default" or "recommended" for analytics.
Reminder Registration and De‑duplication
- Given a next check-in is scheduled, When scheduling completes, Then exactly one reminder notification is registered for that time. - Given an existing reminder overlaps within ±5 minutes for the same room, When registering, Then the system de-duplicates and keeps a single consolidated reminder. - Given push permissions are denied, When registering, Then a local notification is scheduled instead. - Given quiet hours cover the scheduled time, When registering, Then the reminder is auto-shifted to the next allowed minute after quiet hours and the scheduled check-in reflects the shift.
Calendar Integration with Permission Gating
- Given the user toggles "Add to calendar," When calendar permission status is unknown, Then the system requests permission following OS guidelines and proceeds only if granted. - Given permission is granted, When scheduling completes, Then a calendar event is created with title "StreakShare Check‑In," correct start time, a 10-minute alert, and a deep link to the room. - Given permission is denied or creation fails, When scheduling completes, Then no event is created and the UI shows a non-blocking notice with retry option. - Given a scheduled check-in is undone or edited, When the calendar event exists, Then the event is deleted or updated to match the new time within 2 seconds.
Timezone and DST Resilience
- Given I scheduled for "Same time tomorrow," When my device timezone changes before the event, Then the reminder and calendar event adjust to the same local wall-clock time in the new timezone. - Given a DST forward shift removes the exact time (missing hour), When adjusting, Then the schedule selects the nearest earlier available minute and records a "DST_adjusted" flag for analytics. - Given a DST backward shift duplicates an hour, When adjusting, Then the schedule keeps a single reminder at the first occurrence and prevents duplicate alerts. - Given adjustments occur, When I view the schedule, Then the UI displays the adjusted local time with a brief "Adjusted for timezone/DST" note.
Skippable UX and Non‑Intrusive Behavior
- Given the prompt is shown, When I tap "Skip," Then the prompt dismisses within 200ms, nothing is scheduled, and no reminder is created. - Given I skipped today, When I remain in the First Win flow, Then I am not re-prompted again in the same session. - Given I use assistive technologies, When the prompt is focused, Then all interactive elements have accessible labels and are reachable via standard navigation order. - Given low-end devices, When the prompt renders, Then frame rate remains above 50 FPS and the prompt loads within 200ms as measured on target device class.
Telemetry and Conversion Measurement
- Given the prompt is displayed, When the session ends, Then analytics record impression, options rendered, and whether the user opted in or skipped, respecting privacy opt-out. - Given a schedule is created, When the reminder is registered, Then analytics record success/failure code and scheduled timestamp. - Given the reminder fires, When the delivery event occurs, Then analytics record delivery receipt or suppression reason (quiet hours, deduped). - Given the next day check-in occurs or is missed, When the 36-hour window closes, Then analytics attribute conversion (success/miss) to this scheduling decision. - Given intermittent connectivity, When events cannot be sent immediately, Then events queue locally and are delivered within 5 minutes of regained connectivity with at least 99% success.

Instant Gear

Bundles all the speed essentials into one step: add lockscreen widget and watch complication, enable Widget Blur, and finish passkey sign-in. One tap prepares your devices for ultra-fast, discreet check-ins so saving streaks never competes with your meetings or focus blocks.

Requirements

One-Tap Gear Setup Orchestrator
"As a busy remote worker, I want to set up all fast-check-in tools in one tap so that I can save my streaks without breaking focus."
Description

Provide a single-press Instant Gear entry point that sequences all prerequisite actions—capability detection, lockscreen widget add, watch complication provisioning, widget blur enablement, and passkey sign-in—into a guided, minimal-friction flow. The orchestrator displays real-time progress, handles system prompts, retries transient failures, and ensures idempotency so repeated runs only perform missing steps. It integrates with platform deep links and companion services to minimize manual steps, and concludes with a confirmation state that guarantees devices are primed for ultra-fast check-ins.

Acceptance Criteria
Happy Path: First-Time Setup Completes Across Phone and Watch
Given an unsigned-in user on a compatible iPhone with a paired compatible Apple Watch, no lock screen widget or watch complication configured, and Widget Blur disabled And network connectivity is available When the user taps the "Instant Gear" entry point Then the orchestrator shows a progress UI with steps: Detect Capabilities → Add Lock Screen Widget → Provision Watch Complication → Enable Widget Blur → Passkey Sign-In → Verify Readiness → Complete And it deep-links to system UIs to add the lock screen widget and watch complication and returns to the app And Widget Blur is enabled in-app And passkey sign-in completes successfully And each step transitions to Done within 2 seconds of completion And the lock screen widget displays obfuscated content when the device is locked And the flow completes in under 90 seconds without manual navigation beyond prompted confirmations And a confirmation screen shows "Instant Gear Ready" with statuses "Phone Ready" and "Watch Ready"
Idempotent Re-run Skips Completed Steps
Given all Instant Gear steps were previously completed on the device and account When the user taps the "Instant Gear" entry point again Then capability detection runs and previously completed steps are marked "Skipped (Already Done)" And no system prompts are shown And no settings are toggled redundantly And the run completes in under 10 seconds And the confirmation screen is displayed with no changes to existing configurations
Transient Failure Auto-Retry and Recovery
Given a transient network or service error occurs during Passkey Sign-In (e.g., timeout or 5xx) When the orchestrator attempts the Passkey Sign-In step Then it retries up to 3 times with exponential backoff (1s, 2s, 4s) And the progress UI displays "Retrying (n/3)" for each attempt And upon a successful retry, the flow proceeds to the next step and completes And if all retries fail, the step is marked "Failed — Retry Later" and the final state is "Partially Configured" with a visible "Retry" action for that step
User Denials Are Deferred and Resumable
Given the user declines adding the lock screen widget or cancels the passkey prompt When the orchestrator continues the flow Then the declined step is marked "Deferred by User" And remaining eligible steps continue And the completion screen lists deferred steps with contextual "Set Up" buttons And tapping a "Set Up" button re-enters only that step via the relevant deep link or prompt
Capability Detection Drives Conditional Flow
Given the device supports lock screen widgets and Widget Blur but no Apple Watch is paired When the orchestrator runs Then the Watch Complication step is marked "Not Available" and is never attempted And the Lock Screen Widget and Widget Blur steps complete successfully And the confirmation screen shows "Phone Ready" and "Watch Not Available"
Completion Verification Ensures Fast Check-In Readiness
Given at least one check-in surface (lock screen widget and/or watch complication) is configured and the user is signed in When the orchestrator performs final verification Then the lock screen widget deep link launches the app to the check-in screen within 3 seconds And if a watch complication is provisioned, tapping it launches the companion app within 3 seconds And the "Instant Gear Ready" state is only shown if at least one surface passes verification
Lockscreen Widget Fast-Add with Deep Links
"As an iPhone user, I want the lockscreen widget added quickly so that I can check in without unlocking my phone."
Description

Automate and guide lockscreen widget placement by invoking supported system flows and deep links. Preselect the optimal widget variant for quick check-ins, validate successful placement, and provide an inline fallback walkthrough when auto-add is restricted by the OS. Persist configuration in user profile for cross-device consistency and surface health checks if the widget is missing or misconfigured, ensuring reliable one-tap access from the lockscreen.

Acceptance Criteria
Auto-Add Flow Invoked via Deep Link
Given the device OS supports a deep link or API to open the lockscreen widget placement UI When the user taps "Add to Lock Screen" in Instant Gear Then the app opens the system widget placement UI within 2 seconds And the invocation includes an app-scoped callback token to receive the result And upon return the app records a result of "added", "replaced", "canceled", or "no_change" with a timestamp
Optimal Widget Variant Preselected
Given the "Quick Check-In" lockscreen widget variant is defined as the default And the OS supports specifying a widget identifier in the placement request When the app invokes the placement UI Then the request includes the Quick Check-In variant identifier and preferred size for the current device class And if the OS does not allow preselection, the app presents a pre-launch sheet with "Quick Check-In" preselected and labeled Recommended And the chosen variant and size are stored to local config prior to completion for later validation
Successful Placement Validation
Given the user completes the system flow with a result of "added" or "replaced" When the StreakShare lockscreen widget is first rendered Then the widget sends a one-time handshake to the app containing device_id, variant_id, slot_type, and widget_version And upon handshake receipt the app marks widget status=active and displays a success state in Instant Gear And if no handshake is received within 10 minutes, the app marks status=verification_pending and prompts the user to test from the lock screen
Fallback Walkthrough When Auto-Add Blocked
Given the device OS lacks a supported deep link or the deep link invocation fails When the user taps "Add to Lock Screen" in Instant Gear Then the app displays an inline 3–5 step walkthrough tailored to the detected OS version with visuals And provides a single-tap "Open Settings/Lock Screen" CTA when available And includes a "Verify" button that runs the widget health check; on success marks status=active, on failure shows corrective guidance
Persist Widget Configuration to User Profile
Given a widget status is set to active or verification_pending When the app updates the user profile Then it persists per-device widget_config with fields {device_id, os, variant_id, slot_type, status, updated_at} And subsequent devices signed into the same account default to the saved variant_id and slot_type during Instant Gear And profile updates are idempotent and do not overwrite other devices' entries
Health Check for Missing or Misconfigured Widget
Given the user opens Instant Gear or the app runs a daily background health check When the app checks for a widget heartbeat/handshake within the last 7 days and validates variant/slot match Then if the widget is missing, disabled, or mismatched, the app surfaces a non-blocking alert with a "Fix" CTA that re-invokes the add flow And if all checks pass, the app surfaces a green "Lockscreen ready" indicator with last verified timestamp
One-Tap Check-In Launch From Lockscreen
Given the StreakShare lockscreen widget is active When the user taps the widget from the lockscreen Then the app launches directly to the Quick Check-In screen within 1.5 seconds on warm start and 3.0 seconds on cold start And if the user is not authenticated, passkey sign-in is prompted and upon success returns to Quick Check-In without losing context And telemetry records launch time, outcome, and source=lockscreen_widget for at least 95% of taps
Watch Complication/Tiles Quick Provisioning
"As an Apple Watch user, I want a complication set up so that I can check in discreetly during meetings."
Description

Detect paired smartwatches, install or link the companion experience, and guide users to add a complication or tile optimized for single-tap check-ins. Provide deep links to watch face edit screens, show live previews of the complication state, and verify data source connectivity. Cache the chosen complication layout and surface remediation steps if the complication is removed or the watch becomes unpaired, maintaining discreet, on-wrist access.

Acceptance Criteria
Paired Smartwatch Detection and Selection
Given the user opens Instant Gear When smartwatch detection begins Then all compatible paired smartwatches are listed with name, OS, and model within 3 seconds And if more than one device is found, the user can select exactly one to continue And if no compatible device is found, a "Pair a smartwatch" CTA is shown and the "Continue" button is disabled And an analytics event "watch_detected" is logged with device_count
Companion App Install/Link Flow
Given a selected compatible smartwatch And the StreakShare companion is not installed or not linked When the user taps "Install/Link" Then the appropriate store/installer opens to StreakShare companion within 2 seconds And upon returning to StreakShare, installation/link status is detected within 10 seconds And on success, the flow advances to "Add Complication/Tile" with a success confirmation And on failure, a descriptive error is shown with "Retry" and "Troubleshoot" options And an analytics event "watch_link_result" is logged with result: success|fail and error_code when applicable
Deep Link to Add Complication/Tile
Given the smartwatch is linked When the user taps "Add to watch face" Then the platform's editor opens to the face/tile configuration context for the selected watch within 3 seconds And the StreakShare complication/tile is preselected or clearly highlighted in the list And after the user completes editor actions and returns, StreakShare verifies placement within 10 seconds And on success, the step shows status "Added" and enables "Verify connection" And if deep link is unsupported, in-app step-by-step instructions with screenshots are presented and a "Mark as done" control is provided
Live Preview of Complication State
Given the smartwatch is linked and the user is signed in When the provisioning screen loads Then a live preview renders the complication/tile state showing icon, status label, and streak count placeholder conforming to brand styles And when network connectivity toggles offline, the preview updates to an "Offline" state within 2 seconds And when the user completes a check-in on mobile, the preview updates to "Checked in" state within 5 seconds And no personally identifiable habit names are shown in the preview
Data Source Connectivity Verification
Given the smartwatch is linked and the complication/tile is added When the user taps "Verify connection" Then a round-trip ping reaches the watch and returns a signed heartbeat payload within 5 seconds And payload includes app_version, auth_state: valid|invalid, and device_time_offset_ms And on success (valid auth and offset <= 2000 ms), a green check and "Connected" label are displayed And on failure, the user sees a specific cause: Not Installed, Not Granted Permission, Bluetooth Off, Out of Range, or Auth Required, each with a one-tap remediation And the user can retry verification without leaving the screen
Cache Layout and Detect Removal/Unpairing
Given the complication/tile was successfully added When the user completes provisioning Then the selected complication/tile layout (slot, size, face/tile id) is cached locally And if the complication/tile is removed from the watch face, StreakShare detects this within 60 seconds of app foreground or next sync and shows "Complication missing" with "Re-add" using cached layout And if the watch becomes unpaired, StreakShare marks status "Watch unpaired" within 60 seconds and offers "Pair new watch" and "Forget device" And all remediation actions are logged with result status
Discreet Default Complication Layout
Given a fresh install with no prior layout cached When the user adds the StreakShare complication/tile Then the default layout uses a neutral icon and numeric streak indicator without habit names or times And the on-device preview and final placement match the default layout exactly And a settings toggle "Discreet mode" is on by default and applied to the complication/tile And disabling "Discreet mode" does not expose habit names on the watch face
Passkey Sign-In Finalization & Account Linking
"As a returning user, I want to finalize passkey sign-in so that logging in across my devices is instant and secure."
Description

Complete passkey enrollment for the user’s account to enable instant, secure sign-in across devices. Detect existing credentials, link passkeys to the current profile, and gracefully migrate users from legacy authentication without interrupting sessions. Support platform authenticators and secure backup, with fallbacks to passwordless alternatives when passkeys are unavailable. Confirm success with a device-bound credential check to ensure frictionless future access.

Acceptance Criteria
First-Time Passkey Enrollment via Instant Gear
Given an authenticated user initiates Instant Gear and selects "Finish passkey sign-in" When the platform authenticator prompt appears Then a new discoverable passkey is created with userVerification=required for the correct RP ID and linked to the current profile And the server stores credentialId and publicKey and marks the account passkey-enabled And the UI confirms "Passkey ready" and marks the step complete within 1 second of server confirmation And on transient failure, one automatic retry is attempted; otherwise a clear error is shown and no partial credential is stored
Detect and Link Existing Passkey to Current Profile
Given the device holds an existing passkey for the RP ID and the user is signed in to the matching account When Instant Gear checks for existing credentials via a WebAuthn get() request (mediation: conditional) Then possession is proven and the credential is linked to the current profile without creating duplicates And if the credential maps to a different account, linking is blocked and the user is prompted to switch accounts or cancel And the check completes within 3 seconds and is recorded in the audit log
Migrate from Legacy Authentication Without Session Interruption
Given a user is logged in via a legacy passwordless method with an active session When passkey enrollment completes on the current device Then the current session remains active and tokens are not invalidated And other active device sessions remain unaffected And the account retains legacy passwordless as a fallback unless the user explicitly disables it And no profile, streak, or notification settings are altered by the migration
Cross-Device Sign-In with Passkey and Passwordless Fallback
Given the user has an enrolled passkey available on Device B (e.g., via platform keychain) When the user taps "Sign in" on Device B Then a passkey prompt is presented and successful sign-in completes within 5 seconds without text input And if no passkey is available or the user cancels twice, a passwordless alternative (e.g., magic link) is offered within 3 seconds And after a fallback sign-in succeeds, the user is prompted to enroll a passkey on Device B before completing Instant Gear
Device-Bound Credential Verification After Enrollment
Given passkey enrollment is marked complete on a device When the user selects "Verify device access" in Instant Gear Then a WebAuthn assertion with userVerification=required succeeds using the stored credential And the server validates the signature against the stored public key and returns success And the app records a device_verified timestamp and enables one-tap check-ins for subsequent sessions
Graceful Handling When Passkey Is Unavailable or Canceled
Given a user attempts passkey enrollment or sign-in and the platform authenticator is unavailable or the user cancels When the operation fails Then the app shows a clear, non-technical message within 1 second and offers a passwordless alternative in 2 or fewer taps And no partial credentials are persisted client- or server-side And the failure is logged with a diagnostic code and without storing biometric or authenticator secrets
Widget Blur Auto-Enable & Quick Toggle
"As a privacy-conscious user, I want widget blur enabled by default so that my habits aren’t visible to others."
Description

Enable privacy-focused blurring for all surfaces configured during Instant Gear by default, masking habit names and sensitive streak data on lockscreen and watch views. Provide a unified toggle within the setup confirmation and gear settings, with granular controls per widget/complication. Ensure blur states persist across sessions and devices, and honor screenshot and notification privacy where applicable to keep check-ins discreet.

Acceptance Criteria
Default Blur Enabled on Instant Gear Setup
Given a user completes Instant Gear and has added at least one lockscreen widget and/or watch complication When the setup finishes Then blur is enabled by default for every newly configured surface And habit names, goal numbers, and streak counts are obscured on those surfaces And the default blur state is saved to the user profile
Unified Blur Toggle in Setup Confirmation
Given the setup confirmation screen is displayed after Instant Gear Then a "Blur all surfaces" toggle is visible and ON by default When the user turns the toggle OFF Then blur is disabled on all configured surfaces within 1 refresh cycle (<=2 seconds) and the stored blur state updates to OFF When the user turns the toggle ON Then blur is re-enabled on all configured surfaces within 1 refresh cycle and the stored blur state updates to ON Then the toggle state persists when revisiting the confirmation screen and in Gear Settings
Per-Surface Blur Controls in Gear Settings
Given the user opens Gear Settings and navigates to the Blur section Then each installed surface (each lockscreen widget instance and each watch complication) is listed with an individual blur toggle When the user toggles blur for a specific surface Then only that surface's blur state changes within 1 refresh cycle; others remain unchanged Then per-surface blur states persist across app restarts and device reboots
Blur State Persistence and Cross-Device Sync
Given the user is signed in with the same account on multiple devices When they change any blur setting on Device A Then the same setting is reflected on Device B within 60 seconds of being online or on next app foreground, whichever comes first When the user signs out and back in or reinstalls the app Then previously saved blur states are restored from the server When a surface does not exist on a device (e.g., no watch) Then no orphan settings are applied and no errors are shown
Screenshot and Notification Privacy
Given blur is ON for a surface When the user takes a screenshot of that surface Then sensitive fields (habit names, streak counts, goal progress) remain obscured in the captured image When system notifications are generated for check-ins or streak status while blur is ON Then notification content hides sensitive fields and shows only a non-sensitive summary When blur is OFF Then screenshots and notifications include full content
Quick Toggle Responsiveness and Reliability
Given any blur toggle (global or per-surface) is interacted with When the user toggles it Then the control provides immediate visual feedback (<200 ms) And the effective blur state on the corresponding surfaces matches the toggle within 1 refresh cycle (<=2 seconds) Then in a reliability test of 100 rapid toggles, the applied state matches the final toggle selection 100% of the time Then enabling blur adds no more than 50 ms median latency to check-in actions compared to blur OFF
Capability Detection & Graceful Fallbacks
"As a user on different devices, I want the app to detect what my device supports so that I only see relevant setup steps."
Description

Run a preflight scan to detect OS versions, device form factors, and paired wearables, then dynamically tailor the Instant Gear steps. Skip unsupported items with clear reasoning, substitute manual guides where automation is constrained, and avoid presenting irrelevant prompts. Log detection outcomes to improve reliability while keeping user data private, ensuring a streamlined setup path that matches each user’s environment.

Acceptance Criteria
Preflight Scan Tailors Steps on iOS Device Without Paired Watch
Given an iPhone running iOS 16 or later with a platform authenticator available and no paired smartwatch detected When the user taps Instant Gear Then the preflight identifies platform=iOS, lockscreenWidgetSupported=true, widgetBlurSupported=true, passkeySupported=true, watchPaired=false And the Instant Gear flow displays exactly three steps: "Add Lock Screen Widget", "Enable Widget Blur", and "Finish Passkey Sign-In" And no watch-related step (e.g., "Add Watch Complication") is shown And a non-blocking summary note reads "Watch step skipped: no paired watch detected" And no watch permissions or install prompts are presented And completion is achieved only when all displayed steps are finished And a detection outcome event is recorded with non-PII fields reflecting these capabilities
Watch Complication Skipped for Unsupported watchOS Version
Given a phone paired to a smartwatch with watchOS version below the minimum required for complications When the user runs Instant Gear Then the preflight sets watchPaired=true and watchComplicationSupported=false And the "Add Watch Complication" step is omitted from the flow And the summary shows reason text: "Skipped: Complications not supported on your watchOS version" And a "Learn how to upgrade" link is available and opens platform upgrade guidance And no prompts to install or configure the watch app/complication are displayed And all other eligible steps are still presented and functional
Lockscreen Widget Unsupported: Step Skipped with Reason
Given a device/OS where lockscreen widgets are not available When Instant Gear runs Then the preflight sets lockscreenWidgetSupported=false And the "Add Lock Screen Widget" step is not displayed And a summary note states: "Skipped: Lock screen widgets are not supported on this device" And no lockscreen-related permission or settings prompts are shown And other applicable steps (e.g., watch complication, passkey, Widget Blur) remain visible and actionable
Manual Guide Substitution When Automation Is Constrained (Lock Screen Widget)
Given the OS supports lockscreen widgets but the app cannot automate or deep-link directly to add the widget When the user selects "Add Lock Screen Widget" Then the app presents an in-app guide with numbered steps and, where available, a deep link to the closest system screen And the step is marked complete only after the app detects a successful widget handshake within 30 seconds of returning to the app And if no handshake is detected, the step remains incomplete and offers a retry option And telemetry records that a manual guide was shown instead of automation (non-PII)
Passkey Capability Detection with Fallback Enrollment
Given the device lacks a platform authenticator or the user has disabled the required keychain/password manager When Instant Gear reaches the sign-in step Then the preflight sets passkeySupported=false And the "Finish Passkey Sign-In" step is replaced with a fallback "Verify Sign-In via Email/Code" And no passkey prompt is presented And the reason text "Passkeys unavailable on this device" is shown And upon successful fallback verification, the user is authenticated and the flow continues without error And an offer to enroll a passkey later is queued and not shown during Instant Gear
Prior Completion Recognition Suppresses Duplicate Prompts
Given the user already has the lockscreen widget installed, Widget Blur enabled, and is signed in When Instant Gear runs Then the preflight detects prior completion for those items And the flow marks those steps as "Already set" without showing prompts or CTAs And only unmet, applicable steps (e.g., watch complication if supported) are presented And completion is allowed immediately if no unmet steps remain
Privacy-Preserving Logging of Detection Outcomes
Given preflight detection completes When logging the outcome Then only the following fields are recorded: deviceOS major.minor, deviceType (phone/tablet), lockscreenWidgetSupported (bool), widgetBlurSupported (bool), watchPaired (bool), watchOS major.minor (if paired), passkeySupported (bool), stepsShown (list), stepsCompleted (list), eventTimestamp (hour-level precision) And no PII or unique device identifiers (e.g., email, phone number, exact device name, advertising ID) are stored And logs respect user analytics opt-out and are buffered for later transmission if offline And unit tests assert the absence of prohibited fields and the presence/format of allowed fields
Setup Summary, Undo & Rollback Controls
"As a cautious user, I want an easy way to review and undo the setup so that I can revert changes if something isn’t right."
Description

Present a post-setup summary that confirms what was configured and provides single-tap controls to undo or adjust each item. Maintain a reversible state record to safely roll back widget blur, watch complication preferences, and guidance for removing widgets when direct removal is restricted. Offer a Reset Gear action that restores pre-setup defaults while preserving account integrity, promoting user confidence and control.

Acceptance Criteria
Display Post-Setup Summary Immediately
Given the user completes Instant Gear setup in the current session When the Setup Summary screen appears Then it renders within 1000 ms and lists the status of Lockscreen Widget, Watch Complication, Widget Blur, and Passkey Sign-In And items configured in this session are labeled "Configured" with a green check and show a single-tap "Undo" and (if applicable) "Adjust" And items not configured show status "Not Configured" with a "Set Up" action And each configured item shows a timestamp accurate to the minute in the device locale
Single-Tap Undo for Each Configured Item
Given the Setup Summary is visible and an item shows an "Undo" action When the user taps "Undo" for Widget Blur Then the app disables Widget Blur within 500 ms, persists the change, updates the summary to "Off", and shows a confirmation toast And when the user taps "Undo" for Watch Complication preferences Then the app restores the pre-setup preference state within 500 ms and, if direct removal from the watch face is restricted, presents a guided removal sheet
Rollback Widget Blur Setting with State Preservation
Given Widget Blur was enabled by Instant Gear and a reversible state record exists When the user rolls back Widget Blur via "Undo" or via "Reset Gear" Then the exact pre-setup blur value is restored, the state record logs the rollback with a timestamp and actor "user", and the check-in view reflects the new blur setting immediately
Adjust Watch Complication Preferences from Summary
Given the Setup Summary is visible When the user taps "Adjust" on Watch Complication Then the app navigates within 500 ms to the Watch Complication settings screen with current values preselected And saving changes persists them, returns to the summary, and updates the status within 1000 ms
Guided Removal for Lockscreen Widget
Given the Setup Summary is visible and the Lockscreen Widget shows "Configured" When the user taps "Undo" for the Lockscreen Widget Then the app presents a step-by-step guide with OS-version-specific instructions and a deep link to the system Edit Lock Screen UI when available And the guide provides a "Mark as removed" action; after confirmation, the summary updates to "Not Configured" and records the action; if canceled, no status change occurs
Reset Gear Restores Pre-Setup Defaults Without Affecting Account
Given the Setup Summary is visible When the user taps "Reset Gear" and confirms in a modal Then the app atomically restores pre-setup defaults for Widget Blur and Watch Complication preferences within 2 seconds And passkey sign-in and account data remain intact and the user stays signed in And the summary reflects restored defaults and shows a success toast And if any rollback step fails, all changes are reverted to the prior state and an error banner with a retry option is shown
Reversible State Record Persistence and Integrity
Given Instant Gear modifies any configurable item When a change or rollback occurs Then a reversible state record stores setting name, old value, new value, timestamp, and source (Instant Gear, Undo, or Reset Gear) And the record persists across app restarts and OS reboots and supports restoration even after app relaunch And records for affected items are cleared only upon successful Reset Gear completion

Window Fit

Calibrates your initial rolling window and soft grace buffer from recent sleep/wake patterns and calendar rhythms. Shows a clear preview of your Day‑1 window on a timeline so you know exactly how much runway you have to check in—no settings spelunking required.

Requirements

Sleep/Wake Pattern Ingestion
"As a remote worker, I want the app to learn my typical sleep and wake times so that my check-in window aligns with when I’m actually awake and available."
Description

Ingest recent sleep and wake signals from trusted sources (e.g., Apple Health, Google Fit, device usage heuristics, or manual input) to build a rolling 14–21 day profile of a user’s circadian rhythm. Normalize for outliers, naps, travel, DST shifts, and weekends vs. weekdays. Persist a lightweight summary model (median wake time, sleep time, variance bands) tied to the user profile and refresh it daily. Provide a fallback when no sensor data is available by prompting for quick manual entries. Expose a consistent, privacy-preserving interface for downstream components to request current rhythm parameters without accessing raw data.

Acceptance Criteria
Multi-Source Sleep/Wake Ingestion & Deduplication
Given the user has connected Apple Health and/or Google Fit and device-usage heuristics are enabled And there are recorded sleep sessions within the last 21 calendar days possibly overlapping across sources When the ingestion job runs Then the system imports sleep start and end timestamps, wake times, and timezone offsets from each source And deduplicates overlapping sessions by merging segments with gaps less than 15 minutes and resolving conflicts using the priority order: Apple Health, Google Fit, heuristics, manual And excludes sessions shorter than 2 hours and longer than 16 hours from primary sleep, labeling them as naps or invalid respectively And completes ingestion within 10 seconds for 21 days of data and retries up to 3 times on transient errors before surfacing a recoverable error And records per-night source attribution and ingestion timestamp
Rolling 14–21 Day Rhythm Profile Computation
Given at least 14 distinct nights of primary sleep are available within the trailing 21 days When the profile computation runs Then the system computes median local sleep time and median local wake time using timezone-normalized dates And computes variance bands as ±1.4826 × MAD (minutes) for sleep and wake, rounded to the nearest 5 minutes And excludes outlier nights where sleep or wake deviates by more than 120 minutes from the provisional median or is flagged as travel/DST transition And produces separate medians for weekdays (Mon–Fri) and weekends (Sat–Sun) and stores both And marks the profile as "insufficient_data" if fewer than 14 nights are available
DST and Travel Normalization
Given a DST shift of ±60 minutes occurs in the user's locale within the trailing 21 days When computing sleep and wake times Then timestamps are adjusted to the user's local clock time and the DST night is excluded from outlier detection And the DST shift does not create duplicate or missing nights in the 21-day window Given the device timezone changes by ≥2 hours within a 24-hour period (travel) When computing the profile Then the first full night after the change becomes the new local anchor; the night of travel is excluded; prior-night data are converted to the new timezone before median computation
Daily Refresh and Persistence of Summary Model
Given the day boundary passes (03:00 local time) or new data arrive When the refresh job runs Then a summary model is persisted to the user profile with fields: median_sleep_local, median_wake_local, sleep_variance_min, wake_variance_min, weekday_medians, weekend_medians, nights_count, last_updated_at And the stored object size does not exceed 1 KB And repeated runs with unchanged source data produce identical model values (idempotent) And the operation succeeds atomically; partial writes are not visible And last_updated_at reflects the current refresh time in ISO 8601 with timezone
Manual Fallback Prompt and Provisional Model
Given no connected health sources or zero successful ingestions in the last 72 hours When the user opens onboarding or Window Fit setup Then the app prompts for quick manual entries requiring date, sleep start, and sleep end with defaulting to last night And each manual entry can be completed in under 30 seconds and validated (sleep < 16 hours, start before end) And once at least 7 manual nights within the last 14 days exist, a provisional profile is computed; once at least 14 nights exist, it becomes a full profile And manual entries are included with source attribution "manual" and are eligible for the same outlier rules And if fewer than 3 nights exist, the API returns "insufficient_data" with nights_count
Privacy-Preserving Rhythm Parameters API
Given an authorized downstream component requests rhythm parameters When it calls GET /v1/rhythm-parameters Then the response contains only: median_sleep_local, median_wake_local, sleep_variance_min, wake_variance_min, weekday_medians, weekend_medians, nights_count, freshness_ts, confidence_level, and version And the response excludes raw sleep/wake events, per-night records, and source identifiers And the request is authorized via service-to-service token; unauthorized requests receive HTTP 401; forbidden receive 403 And P95 latency for the endpoint is ≤200 ms and uptime is ≥99.9% And the response includes Cache-Control: max-age=300 and ETag for versioning
Calendar Rhythm Sync
"As a creator with a packed calendar, I want my check-in window to avoid my recurring meetings so that I can keep my streak without interrupting work."
Description

Connect to users’ calendars (Google, Outlook, iCloud) with explicit consent to detect recurring commitments and preferred working hours. Derive a non-sensitive rhythm profile (busy blocks, focus time, typical start/end of workday) to refine window placement and avoid obvious conflicts. Support multiple calendars, time zones, and read-only metadata ingestion. Provide incremental sync, conflict resolution, and graceful degradation when permissions are revoked or data is stale.

Acceptance Criteria
Calendar Consent and Connection
Given the user opens Settings and taps Connect Calendar When they select Google, Outlook, or iCloud and complete OAuth Then the app requests read-only calendar scopes only And event titles, descriptions, attendees, organizer emails, and locations are neither fetched nor stored at rest And only event start time, end time, busy/free status, recurrence rule, calendar ID, event time zone, and last-modified timestamps are ingested And on success, the connection list shows provider name, connected calendar count, and last successful sync time And on denial or error, no calendar data is persisted and the user sees a retryable, non-destructive error state
Non-Sensitive Rhythm Profile Derivation
Given at least 7 calendar days of metadata are available When the rhythm profile is generated Then the profile computes typical_work_start_time and typical_work_end_time as the median first and last busy block per weekday in the user’s local time And it identifies recurring_busy_windows with fields: weekday, start, end, and confidence in [0.0,1.0] And it identifies focus_time_blocks as contiguous busy spans ≥ 60 minutes And the profile contains no raw event titles, descriptions, attendee lists, locations, or organizer emails And derivation uses only start/end times, recurrence rules, busy/free flags, and time zones as inputs
Multi-Calendar Aggregation and Conflict Resolution
Given multiple calendars from one or more providers are connected When events overlap or duplicates exist across calendars Then the effective busy timeline used for profile derivation is the union of all busy blocks after de-duplication And duplicates are merged by stable event UID when available; otherwise events with identical start/end within 5 minutes on the same day are treated as one And in free/busy conflicts over the same time span, busy takes precedence over tentative and free And OOO events are treated as busy for conflict purposes
Time Zone and DST Handling
Given calendars include events in multiple time zones and a period spanning a DST transition When events are normalized for analysis and preview Then times are converted to UTC internally and rendered in the user’s current local time for profile outputs and previews And DST transitions do not shift typical_work_start_time or typical_work_end_time by more than 60 minutes relative to adjacent weeks And events with explicit time zones preserve their local wall-clock times when displayed in previews after conversion
Incremental Sync and Staleness Management
Given an initial successful calendar sync has completed When subsequent syncs run Then only changes since the last sync are requested using provider change tokens or sync anchors And a foreground sync is attempted within 5 seconds of app open and completes within 10 seconds for up to 5,000 events, or a visible progress indicator is shown And background syncs back off on 429/5xx with exponential delays up to 24 hours while retaining the last good profile And the UI shows the last successful sync time and marks the profile as Stale if older than 24 hours And if freshness exceeds 72 hours, the profile is frozen and no window adjustments are applied until a successful sync completes
Permission Revocation and Graceful Degradation
Given the user disconnects calendars in-app or revokes access at the provider When the next sync attempt occurs Then invalid credentials are detected, tokens are securely removed, and the calendars are marked Disconnected And no further fetch attempts are made until the user reconnects And the last computed rhythm profile is retained locally and labeled Stale — calendar disconnected And Window Fit falls back to sleep/wake-derived heuristics only and surfaces a non-blocking CTA to reconnect
Day‑1 Window Preview Conflict Avoidance
Given a rhythm profile is available When the Day‑1 window preview is rendered Then the initial check-in window is placed to avoid overlap with recurring_busy_windows and focus_time_blocks And if complete avoidance is impossible, the chosen window minimizes total overlap and displays a conflict indicator And the preview updates within 10 seconds after a successful sync or profile regeneration And in conflict-free cases, overlap between the previewed window and busy blocks is ≤ 10 minutes
Rolling Window Calibration Engine
"As a new user, I want the app to set a smart starting window for me so that I can begin streaking without tweaking settings."
Description

Compute the initial daily check-in rolling window using aggregated sleep/wake and calendar rhythm profiles. Apply guardrails (minimum/maximum window lengths, earliest/latest allowable anchors) and adapt by day-of-week. Output a deterministic window start/end plus confidence score and rationale for auditability. Persist the baseline for Day‑1 and schedule periodic re-calibration (e.g., weekly) with change-diff limits to prevent volatile shifts. Respect user overrides and per-habit nuances where applicable, while maintaining a sensible global default.

Acceptance Criteria
Compute Deterministic Day‑1 Window with Confidence and Rationale
Given aggregated sleep/wake and calendar rhythm profiles meeting MIN_DAYS_REQUIRED valid days When the calibration engine runs for a user with no overrides Then it produces window_start_utc and window_end_utc as RFC3339 timestamps in UTC, where duration = end − start and MIN_WINDOW_HOURS ≤ duration ≤ MAX_WINDOW_HOURS And it returns confidence_score ∈ [0.00,1.00] with precision 0.01 And it returns rationale as a non‑empty ordered list describing key contributors and any guardrail applications And the output includes schema fields: version, inputs_hash, window_start_local, window_end_local, tz_id, duration_hours, confidence_score, rationale[] And re‑running with identical inputs_hash yields identical outputs (deterministic)
Apply Guardrails and Day‑of‑Week Adaptation
Given configured guardrails MIN_WINDOW_HOURS, MAX_WINDOW_HOURS, EARLIEST_ANCHOR_LOCAL, LATEST_ANCHOR_LOCAL and day_of_week_offsets When computing windows for the next 7 calendar days Then for each day, window_start_local ≥ EARLIEST_ANCHOR_LOCAL and window_start_local ≤ LATEST_ANCHOR_LOCAL And duration_hours ∈ [MIN_WINDOW_HOURS, MAX_WINDOW_HOURS] And day_of_week_offsets are applied exactly to the natural window anchors derived from profiles And any clamping due to guardrails is recorded in rationale with the affected field and magnitude
Persist Baseline and Use for Day‑1
Given a Day‑1 baseline window is computed When the baseline is persisted Then a single immutable record is stored with fields: window_start_utc, window_end_utc, tz_id, duration_hours, confidence_score, rationale[], version, created_at, inputs_hash And subsequent Day‑1 retrieval returns the persisted baseline without recomputation And recomputation on Day‑1 does not overwrite the baseline unless an explicit user override is applied (recording override source and timestamp)
Schedule Weekly Re‑calibration with Change‑Diff Limits
Given a weekly recalibration schedule at RECALC_HOUR_LOCAL When the scheduled job runs Then the engine recalculates using the most recent data and produces new candidate window values And it computes change_diff_start_hours and change_diff_end_hours relative to the current baseline And if any absolute diff exceeds MAX_WEEKLY_SHIFT_HOURS, the new values are clamped to the limit and rationale notes the clamp And if valid input days < MIN_DAYS_REQUIRED, recalibration is skipped, prior baseline is retained, and skip_reason is logged And an audit record is written containing previous_window, candidate_window, applied_window, diffs, confidence, rationale, and job_id
Respect User Overrides and Per‑Habit Nuances
Given a user has defined a global override or a per‑habit override When the engine computes the window for Day‑1 or during recalibration Then per‑habit overrides take precedence over global overrides and both take precedence over computed values And guardrails are still enforced; any adjustments due to guardrails are included in rationale And removing an override causes the next run to fall back to the last valid baseline/computed window And all override applications and removals are recorded with actor, source, and timestamp in the audit log
Handle Timezone, DST, and Travel Robustly
Given the user’s tz_id and any pending timezone changes or DST transitions When computing windows across a timezone change or DST boundary Then window_start_local and window_end_local remain aligned to intended local wall‑clock anchors, subject to guardrails And persisted timestamps are stored in UTC with tz_id and local offsets captured at computation time And on days with DST transitions, duration_hours reflects the real elapsed time while maintaining local anchor alignment where possible within guardrails And after a timezone change effective_at, the next day’s computation uses the new tz_id and local calendar rhythms And audit records include tz_context and any adjustments made due to DST/timezone changes
Soft Grace Buffer Logic
"As a habit tracker, I want a small grace period around my window so that a few minutes of delay don’t break my streak."
Description

Implement a configurable grace buffer that extends eligibility slightly before/after the calibrated window to prevent streak loss due to minor timing slips. Define default thresholds based on rhythm confidence and recent adherence, with caps per week to prevent abuse. Clearly label buffer-assisted check-ins in analytics, and avoid retroactive changes after the day closes. Ensure deterministic behavior across time zone changes and DST, and provide guardrails for edge cases (e.g., back-to-back buffers).

Acceptance Criteria
Soft Buffer Eligibility Around Calibrated Window
Given a user has a calibrated daily window of 08:00–10:00 local time and a soft buffer of 10 minutes pre and 10 minutes post When the user attempts to check in at 07:55, 08:30, and 10:07 Then 07:55 and 10:07 are accepted as eligible and labeled Grace-Assisted, and 08:30 is accepted and labeled On-Time And any attempt before 07:50 or after 10:10 is rejected as ineligible And eligibility and labeling are computed at check-in time using the active buffer for that day
Adaptive Default Buffer Based on Rhythm Confidence and Adherence
Given rhythm_confidence and adherence_7d are available for the user When rhythm_confidence >= 0.80 and adherence_7d >= 6 Then the default buffer is 15 minutes pre and 15 minutes post for the next local day When 0.50 <= rhythm_confidence < 0.80 or 3 <= adherence_7d <= 5 Then the default buffer is 10 minutes pre and 10 minutes post for the next local day When rhythm_confidence < 0.50 or adherence_7d <= 2 Then the default buffer is 5 minutes pre and 5 minutes post for the next local day And the default buffer never exceeds 15 minutes per side or 30 minutes total per day And if the user sets a manual buffer within 0–15 minutes per side, it overrides the default starting the next local day; no past days are changed
Weekly Cap on Grace-Assisted Check-ins
Given a rolling 7-day window with a cap of 3 Grace-Assisted check-ins When the user performs 4 or more check-ins that fall only within the buffer during this period Then only the first 3 such check-ins are accepted and labeled Grace-Assisted and count toward the streak And any additional check-ins that are only buffer-eligible within the same 7-day window are rejected as ineligible And on-time check-ins do not consume the cap And the cap rolls forward so that when the oldest Grace-Assisted check-in exits the 7-day window, one new Grace-Assisted check-in becomes eligible
Analytics Labeling for Grace-Assisted Check-ins
Given a check-in occurred within the soft buffer When viewing the daily timeline, streak summary, and analytics/export Then the event is visibly labeled Grace-Assisted in UI views and excluded from On-Time rate calculations And analytics show counts of Grace-Assisted check-ins per week and per streak And data export includes fields grace_assisted=true, buffer_side=pre|post, buffer_minutes_used, and classification_version And these labels and fields are immutable after the day closes
No Retroactive Changes After Day Close
Given the local day closes at 24:00 in the user’s active time zone at the time of the events When a day has closed Then buffer sizes, eligibility decisions, labels, and streak status for that day remain fixed even if rhythm inputs, calendar data, settings, or time zone later change And manual configuration changes take effect from the next local day and do not reopen or reclassify prior days
Deterministic Behavior Across Time Zone Changes and DST
Given check-ins are stored with UTC timestamp, local timestamp, and time zone offset at event time When the user changes time zones or DST transitions occur Then re-evaluating any past check-in yields the same eligibility and labeling result as originally computed And DST forward transitions do not create missing eligibility, and DST backward transitions do not create duplicate eligibility And the calibrated window and soft buffer for each day are anchored to the local day and time zone active at event time
Guardrails for Overlapping Back-to-Back Buffers
Given Day N’s post-buffer overlaps with Day N+1’s pre-buffer When the user checks in within the overlapping interval Then exactly one day is satisfied, preferring Day N while it remains open; otherwise Day N+1 And consuming a buffer for Day N does not consume Day N+1’s weekly cap allowance And two check-ins within the overlapping interval cannot satisfy two consecutive days solely via buffers; at least one must occur within a main window And the soft buffer never extends eligibility beyond the start of the next day’s main window
Day‑1 Timeline Preview UI
"As a first-time user, I want to see exactly when today’s check-in window opens and closes so that I know how much time I have without digging through settings."
Description

Render a clear, accessible timeline preview that highlights the Day‑1 rolling window and soft grace buffer, shows current time and remaining runway, and overlays key calendar busy blocks and sleep anchors (abstracted). Provide responsive layouts (mobile-first), color-blind safe palettes, and haptic/visual states for ‘window open/closing/closed.’ Include skeleton loading, error fallbacks, and tooltips to explain how the window was set—no deep settings navigation required.

Acceptance Criteria
Day‑1 rolling window and soft grace visualization
- Given a computed Day‑1 window start/end and a soft grace buffer, when the preview loads, then the timeline renders a primary window band and a visually distinct soft‑grace segment attached to the correct boundary. - The window band occupies a proportional length of the 24h timeline matching the duration within ±4% tolerance. - The soft‑grace segment is differentiated by pattern/texture or outline, not color alone. - Window and soft‑grace labels display start/end times in the user’s locale (12/24h) and are exposed to assistive tech with meaningful names.
Current time indicator and remaining runway countdown
- Given system time, when the preview is visible, then a current‑time marker appears at the correct position with ≤1‑minute drift and updates at least every 30s. - Remaining runway displays as hh:mm (or mm:ss when <1h) and updates every 1s when <60m, otherwise every 60s. - When current time is outside window+grace, remaining runway reads 00:00 and state switches to Closed. - If the app is backgrounded >5m, on resume the marker and countdown snap to the latest time within 300ms.
Calendar busy blocks and sleep anchors overlay (abstracted)
- With calendar permission granted, busy blocks overlapping the Day‑1 window render as neutral segments without titles or sensitive details. - If permission is denied or no events exist, a non‑blocking message ‘No calendar data’ appears with a CTA to grant access; the timeline remains interactive. - Sleep anchors derived from the last 7 days show as abstract markers or bands with generalized ranges (e.g., ‘sleep 23:00–07:00’), never precise per‑event timestamps. - Overlays never obscure window boundaries; maintain ≥8px separation or bring boundaries to top layer on hover/tap. - Tapping an overlay shows a brief tooltip with abstracted info; ESC/outside tap dismisses.
Responsive layout and color‑blind safe palette
- Mobile portrait 320–428px: vertical stack, timeline min‑height ≥120px, touch targets ≥44×44pt, labels do not truncate critical times. - Tablet 768–1024px and desktop ≥1024px: timeline scales to width with non‑overlapping labels at test breakpoints 375, 414, 768, 1024, 1440. - Color usage meets WCAG 2.2: non‑text graphical contrast ≥3:1, text contrast ≥4.5:1; state encodings remain distinguishable under protanopia, deuteranopia, and tritanopia simulations. - State differences (Open/Closing/Closed/Grace) have redundant cues via pattern/outline, not color alone.
Haptic and visual states for open/closing/closed
- Entering Open: trigger light haptic on supported mobile and animate a 1.5s subtle glow on the window band; no haptic on desktop. - Closing state begins 15 minutes before window end: countdown turns amber, band pulses at 0.5Hz, and an optional nudge haptic fires once. - Transition to Grace: band style switches to patterned grace; CTA text updates to reflect grace usage; analytics event ‘window_to_grace’ fires once. - Closed: band desaturates, check‑in CTA disabled, tooltip explains next window timing. - Honors OS reduced‑motion: animations replaced with instant state change and outline emphasis when enabled.
Skeleton loading and error fallbacks
- On initial load, skeleton timeline appears within 200ms; data replaces skeleton within 2s on a 4G reference network; if >4s, show ‘Still loading…’ status. - If calibration data is unavailable, show a fallback card ‘We’ll estimate your Day‑1 window after your first check‑ins’ with Retry; overlays are suppressed until data loads. - If data fetch fails, display a non‑blocking error with Retry; after 3 consecutive failures, offer ‘Report issue’ link; no crashes or blank screens. - Loading, empty, and error states are announced to screen readers within 1s of state change.
Accessibility: screen reader and keyboard support
- All interactive elements have accessible names and focus order follows visual order; visible focus indicator contrast ≥3:1. - Timeline region exposes a concise accessible description of window start/end, grace duration, and remaining runway; screen readers can cycle markers via arrow keys. - Tooltips open on keyboard focus/Enter/Space and close with ESC or blur; focus returns to the triggering control. - Time strings localize to user locale and respect 12/24h preference; content remains usable in high‑contrast and reduced‑motion modes.
Privacy & Consent Controls
"As a privacy-conscious user, I want clear control over what data is used to set my window so that I can benefit from personalization without sharing more than necessary."
Description

Provide granular consent flows for sleep and calendar data with purpose-specific explanations, data minimization, and easy revocation. Offer local-only processing where possible and clearly indicate what is stored (summaries vs. raw data) and for how long. Centralize controls in Settings, log consent changes, and propagate permission revocations to dependent modules. Ensure secure at-rest/in-transit handling and prepare for regional compliance needs.

Acceptance Criteria
Onboarding Purpose-Specific Consent for Window Fit Data Sources
Given a first-time user starts Window Fit setup When the consent screen is displayed Then the user is shown separate consent controls for Sleep Data and Calendar Data with purpose text: "Calibrate initial rolling window and soft grace buffer" And the screen explicitly lists data categories per source (e.g., sleep/wake times; busy/free blocks) and whether only summaries or raw data are stored And the screen displays retention durations for stored summaries (e.g., last 30 days) and states that raw data is not stored When the user enables a source Then only that source’s OS permission prompt is invoked And the enablement is recorded in a consent log with timestamp, app version, and policy text version When the user declines a source Then no OS permission prompt for that source is invoked and Window Fit proceeds using remaining sources And the Day‑1 window preview shows an impact notice indicating missing source(s) with a link to Settings
Settings: Centralized Privacy Controls and Data Transparency
Given the user navigates to Settings > Privacy & Consent Then the screen shows independent toggles for Sleep Data and Calendar Data, a Local‑Only Processing toggle, and a Revoke All button And a read-only "What we store" section lists stored fields as summaries vs. raw, with retention durations, per source And a "Manage OS Permissions" action deep-links to system settings for each source When the user changes any toggle Then the change takes effect app-wide within 1 second and is appended to the consent log with timestamp and actor (user/app) And the Day‑1 window preview and Window Fit computations reflect the new permissions on next render (<1 second)
Revocation Propagation to Window Fit and Dependent Modules
Given Window Fit has previously used both Sleep Data and Calendar Data When the user revokes Calendar Data in Settings or via OS Then Window Fit recomputes using only Sleep Data within 1 second and updates the Day‑1 window preview with a "Calendar data off" badge And all cached calendar-derived summaries for Window Fit are deleted within 5 seconds and excluded from future computations And background jobs that read calendar are cancelled within 5 seconds and do not restart without renewed consent And no new calendar reads occur (verified by instrumentation logs) after revocation
Local‑Only Processing and Data Minimization Enforcement
Given the user enables Local‑Only Processing Then no sleep or calendar fields or derived metrics are transmitted off-device (verified by network inspection) And only summarized fields required for Window Fit (e.g., recent sleep midpoint, wake time ranges, busy blocks histogram) are stored; raw records are never persisted to disk And the "What we store" list updates to reflect summaries only and shows retention durations When Local‑Only Processing is disabled Then transmissions, if any, exclude raw sleep/calendar records and include only minimized summaries with documented retention and purpose And attempts to export app data exclude raw sleep/calendar records in all cases
Secure Handling: At‑Rest and In‑Transit Protections
Given Window Fit stores summary data for consented sources Then the data is encrypted at rest using platform-secure storage and is unreadable to other apps And raw sleep/calendar records are not stored on disk at any time When any data related to Window Fit is transmitted Then all payloads are sent over TLS 1.2+ with certificate validation; connections with invalid certificates are rejected And payloads exclude raw sleep/calendar records and PII beyond what is explicitly consented
Regional Compliance and User Rights Support
Given the device locale/region is in the EEA or other stricter region When onboarding for Window Fit begins Then all purpose-specific consents default to off and processing is blocked until explicit opt-in is captured And consent copy reflects regional policy with links to Privacy Policy and Data Rights And the consent log records lawful basis as consent with timestamp and policy version When the user requests data deletion for Window Fit data Then all stored summaries for sleep/calendar sources are deleted within 24 hours and the action is logged and confirmed in-app When the user requests data export Then the export includes only minimized summaries, excludes raw records, and is made available within 24 hours
First‑Run Window Fit Onboarding
"As a new user, I want a quick setup that gives me a tailored window right away so that I can start my streak without configuration hassle."
Description

Integrate a lightweight onboarding flow that collects minimal inputs (wake time fallback, weekday/weekend differences, calendar connect) and immediately shows the Day‑1 timeline preview. Provide optional skips, contextual tips, and a quick ‘Try without data’ path. Record onboarding completion, handle interrupted sessions, and route users back to the preview with undo/redo for choices. Ensure localization readiness and analytics hooks to measure drop-off and comprehension.

Acceptance Criteria
Capture Wake Fallback and Weekday/Weekend Variance
Given a first-time user with no prior preferences When the user opens onboarding and sets a default wake time via the time picker Then the value is validated (HH:MM, device locale 12/24h), saved to the profile, and immediately used to compute the Day‑1 window And the Day‑1 timeline preview updates within 500 ms of confirmation Given the user enables a Weekend different wake time option When the user sets a weekend wake time Then the app persists both weekday and weekend wake times and recalculates the Day‑1 window based on the upcoming calendar day‑of‑week Given the user enters an invalid or incomplete time When attempting to proceed Then progression is blocked with an inline error and accessible hint until corrected Given the device time zone changes during onboarding When wake times are saved Then times are stored as local wall times and associated with the current IANA time zone identifier for correct future interpretation Given the user navigates back and forth between steps When returning to the wake time step Then previously entered values are pre-populated without loss
Calendar Connect and Permission Handling
Given the user reaches the calendar connect step When the user taps Connect Then the OS permission prompt for calendar read access is shown once, with an explanation sheet displayed in-app prior to the prompt Given the user selects Allow When permission is granted Then the app ingests event time ranges for the next 14 days (start/end metadata only), derives suggested windows, and updates the Day‑1 preview within 2 seconds And no event titles, descriptions, or invitees are stored; only derived timing summaries are persisted Given the user selects Don’t Allow or taps Skip When returning to the preview Then the Day‑1 window is computed using only the provided wake fallback and any defaults; the user is not reprompted unless they explicitly tap Connect again from the preview Given permission was previously denied When the user taps Connect Then the app shows an in-app instruction to enable access in system settings and does not re-trigger the OS dialog Given the user connects a calendar When visiting Settings Then the user can view connection state and disconnect, after which derived calendar data is cleared and the preview reverts to non-calendar logic
Day‑1 Window Preview and Runway Display
Given inputs are available (wake fallback and/or calendar rhythm) When the preview screen is shown Then a timeline displays the computed Day‑1 rolling window start/end, a soft grace buffer region, and a current time marker And a countdown to check-in deadline is shown to the nearest minute and updates every 60 seconds Given the user taps an info icon on the preview When the tooltip is opened Then contextual copy explains the rolling window and soft grace in <120 characters per tip, is dismissible, and is readable by screen readers Given the device is on small-screen or large font settings When the preview renders Then no critical UI is clipped or overlaps, and all labels remain readable and tappable with a minimum touch target of 44x44 pt Given any input changes (wake time, calendar connect/disconnect) When the user confirms the change Then the preview recalculates and re-renders within 500 ms, reflecting the new Day‑1 window and countdown
Skip Paths and Try Without Data
Given the user is on any onboarding step (wake time, weekend variance, calendar connect) When the user taps Skip Then the step is marked skipped, defaults are applied for calculations, and the user proceeds to the next step without error Given the user is at the onboarding entry When the user selects Try without data Then the flow jumps directly to the Day‑1 preview using default heuristics, with an option to add data later from the preview Given a step was skipped When viewing analytics Then a skip_tapped event is emitted with step identifier, timestamp, and locale (no PII) Given the user later decides to provide skipped data When tapping Add data from the preview Then the app routes to the relevant step, and upon confirmation, returns to the preview with recalculated results
Resume and Undo/Redo from Preview After Interruption
Given the app is terminated or backgrounded during onboarding When it is relaunched within 7 days Then the user is returned to the last completed step, or to the preview if sufficient inputs exist, with all previously saved values intact Given the user made changes on a step but did not confirm When the app is interrupted before confirmation Then unconfirmed changes are not persisted and the last confirmed state is restored on resume Given the user is on the Day‑1 preview When the user taps Undo Then the most recent confirmed change (e.g., wake time adjustment, weekend toggle, calendar connect/disconnect) is reverted and the preview recalculates accordingly within 500 ms Given an undo was performed When the user taps Redo Then the reverted change is reapplied with identical results and recalculation timing Given a long sequence of changes in a single onboarding session When using undo/redo Then up to the last 10 confirmed changes are available in history; history clears upon onboarding completion
Localization and Accessibility Readiness
Given the app is built with localization enabled When switching device locale (e.g., en-US, es-ES, fr-FR) Then all onboarding strings, tooltips, and buttons render from resource files with no hard-coded text and no truncation on standard devices Given a right-to-left locale (e.g., ar) When viewing the onboarding and preview Then layout mirrors appropriately, timelines and labels remain intelligible, and navigation order is logical for RTL Given device time format is 12h or 24h When displaying or editing times Then the app respects the system setting consistently across all onboarding steps and the preview Given a user with assistive technologies When using VoiceOver/TalkBack and Dynamic Type Then all interactive elements have descriptive accessibility labels and hints, focus order is logical, and UI remains usable up to the largest font size Given pseudo-localization is enabled in a test build When running the onboarding Then no clipped text, hard-coded strings, or layout overflows are detected
Analytics Hooks and Completion Recording
Given the user progresses through onboarding When a step is viewed, an input is confirmed, Skip is tapped, Try without data is used, Connect is allowed/denied, a tooltip is opened, or the preview is viewed for >3 seconds Then analytics events are emitted with event name, step id, result, timestamp, app version, and locale; no PII is included; events are queued offline and retried when connectivity is restored Given onboarding is completed from the preview via Continue/Finish When completion occurs Then a persistent profile flag onboarding_completed=true is stored with timestamp and onboarding_version, and the next app launch bypasses onboarding to the main experience Given a user wishes to redo onboarding When navigating to Settings > Onboarding Then a Reset Onboarding option is available that clears the completion flag and any derived onboarding-only data (not habits), allowing the flow to be re-run Given analytics requirements for drop-off and comprehension When reviewing event data Then a funnel can be reconstructed end-to-end with per-step view, interact, skip, and completion rates; an optional single-question comprehension poll response (yes/no) from the preview is captured when presented

Nudge Blueprint

Personalizes your default rescue plan in seconds—choose nudge style (silent haptic, subtle banner), snooze lengths, and escalation cap. Smart presets align to your schedule so reminders feel timely, not naggy, and streak saves stay high even on hectic days.

Requirements

Nudge Style & Channel Selector
"As a remote worker frequently on calls, I want to set silent haptic nudges as my default so that I get reminders without disrupting meetings."
Description

Allows users to set a default nudge modality (silent haptic, subtle banner, standard push, in-app toast) per habit or globally, with live preview and device-capability detection. Provides fallbacks when a modality is unsupported and maps selections to OS notification categories/channels. Centralizes configuration in the Nudge Blueprint screen and propagates defaults to new habits. Benefits include reduced interruption, higher adherence, and consistent UX across habits and devices.

Acceptance Criteria
Global Default Modality Selection with Live Preview
- Given I am on the Nudge Blueprint screen with notification permission granted, When I select a global nudge modality (Silent haptic, Subtle banner, Standard push, In-app toast), Then the selection is saved to my profile and persists after app relaunch. - When a global modality is selected, Then a live preview of that modality is presented within 1 second and can be replayed. - When I create a new habit via any creation flow after setting a global modality, Then the habit’s nudge modality defaults to the global selection unless I explicitly choose another during creation. - When I change the global default later, Then existing habits are not altered retroactively, and the UI indicates that only new habits inherit the new default.
Per-Habit Override and Inheritance
- Given a habit exists and a global default is set, When I open that habit’s settings and choose a different modality, Then the habit stores an override and uses it for its nudges. - When I clear the override by selecting “Use global,” Then the habit immediately adopts the current global default. - Given I duplicate a habit, When the duplicate is created, Then the modality override state (value or Use global) is preserved in the duplicate. - Given a habit is set to “Use global,” When the global default changes, Then the habit’s effective modality updates to the new global value without additional user action.
Device Capability Detection and Fallbacks
- Given the device lacks a haptic engine, When viewing modality options, Then “Silent haptic” is visibly disabled with an “Unsupported on this device” note and cannot be selected. - Given I previously selected an unsupported modality and launch the app on a device without support, Then the app automatically applies the fallback order Subtle banner → Standard push → In-app toast, marks the effective modality in UI, and logs a fallback event with a reason code. - Given OS notification permission is denied, When I select Standard push or Subtle banner, Then the app requests permission; if declined, the chosen modality remains saved but the effective modality becomes In-app toast until permission is granted, with a non-blocking notice. - When device capability or permission status changes (e.g., permission granted), Then the effective modality updates within 5 seconds and the notice clears.
OS Channel/Category Mapping
- For each modality selection, Then the app maps to a single OS category/channel with attributes that match the modality intent: Silent haptic = no sound, vibration/haptic only; Subtle banner = alert/banner, no sound; Standard push = alert/banner with sound; In-app toast = no OS channel. - When a modality is selected and its OS channel/category does not exist, Then the app creates it once and reuses it thereafter; no duplicate channels/categories are created on subsequent selections or app launches. - When I change modalities, Then the app registers/updates the correct OS category/channel within 2 seconds and the OS settings reflect the expected sound/vibration/importance flags. - Android: channels use stable IDs per modality; iOS: categories use stable identifiers per modality; removing/reinstalling the app recreates them on first launch.
In-App Toast Delivery and Preview
- Given the app is in the foreground and the effective modality is In-app toast, When a nudge fires, Then a toast appears within 1 second, is non-blocking, auto-dismisses within 5 seconds, and produces no system sound. - Given the app is in the background and the effective modality is In-app toast, When I return to the app, Then a queued toast appears within 5 seconds of resume labeled “Missed nudge,” limited to one per missed event. - When I tap “Preview” for In-app toast on the Nudge Blueprint screen, Then the preview matches the production toast style (size, placement, duration) exactly. - Accessibility: the toast is announced to screen readers within 1 second and provides a dismiss action.
Centralized Configuration Sync Across Devices
- Given I update the global modality on Device A while online, Then Device B (same account) reflects the new default within 60 seconds or on next launch, whichever is sooner. - When offline on Device A, Then my change is stored locally and syncs within 10 seconds after connectivity is restored; the final server value uses last-write-wins with timestamp conflict resolution. - An audit log entry (user ID, device ID, old → new modality, timestamp) is recorded on the backend for each global modality change. - Telemetry reports propagation latency per device, with p95 under 60 seconds for online devices.
Smart Schedule Presets
"As a creator with variable meeting times, I want reminders to adapt to my calendar and time zone so that nudges arrive when I can act on them."
Description

Offers one-tap presets (e.g., Morning Maker, Workday Flow, Evening Wind-down) that align nudge windows to the user’s local time zone, declared working hours, and each habit’s target times. Optionally reads calendar free/busy and Focus/Do Not Disturb signals (with permission) to avoid interrupting meetings or deep work. Auto-adjusts for travel/timezone changes and weekends. Users can customize and apply presets per habit or globally. Increases timeliness of reminders while reducing perceived nagging.

Acceptance Criteria
Local Time & Working Hours Alignment
Given the device timezone is set to UTC-5 and the user’s working hours are 09:00–17:00 local And the Morning Maker preset’s nudge window is configured as 06:00–09:00 local And a habit’s target time is 07:30 local When the user applies the Morning Maker preset to that habit Then all scheduled nudges for the habit occur between 06:00 and 09:00 local And no nudges are scheduled between 09:00 and 17:00 local And the first nudge is scheduled at 07:30 ±10 minutes
Calendar Busy Suppression with Permission Granted
Given the user has granted calendar access And the calendar shows a busy event from 10:00–11:00 local during the preset’s valid window And the initial nudge time computes to 10:15 local When the schedule is generated Then no nudge is delivered between 10:00 and 11:00 local And the 10:15 nudge is deferred to the first free minute at or after 11:00 local within the same allowed window, or to the next allowed slot if the window has ended And the deferral reason is recorded as "calendar-busy" in the scheduling log And if calendar permission is subsequently revoked, future schedules ignore busy events and record permission state as "revoked"
Focus/Do Not Disturb Respect and Deferral
Given the OS Focus/Do Not Disturb is active from 14:00–15:30 local And a nudge is due at 14:10 local within an allowed window When the schedule is evaluated Then the 14:10 nudge is not delivered during Focus/DND And it is queued for delivery within 5 minutes after 15:30 local if still within an allowed window And if the window has ended, it is moved to the next allowed slot defined by the preset And the deferral reason is recorded as "focus-dnd" in the scheduling log
Automatic Timezone Adjustment on Travel
Given a user’s nudges are scheduled in UTC-8 (PST) for the day And the device timezone changes to UTC-5 (EST) at 13:00 local When the app detects the timezone change Then all future (not yet delivered) nudges are shifted to their equivalent local clock times in UTC-5 while remaining within the preset’s valid windows And any nudge that would be in the past after the shift is rescheduled to the next valid slot And no duplicate nudges are delivered during the transition And the reschedule event is logged with previous and new times
Weekend-Specific Scheduling Behavior
Given a preset defines weekday windows as 06:00–09:00 and weekend windows as 09:00–11:00 local And a habit is marked as "weekdays only" When the date is a Saturday Then no nudges are scheduled for that habit And for habits without weekday-only restriction, nudges scheduled on Saturday occur only between 09:00 and 11:00 local And no nudges are scheduled outside the defined weekend window
Global Preset with Per-Habit Override
Given the user sets the global preset to Workday Flow And the user overrides Habit A to use Evening Wind-down When schedules are generated Then Habit A uses Evening Wind-down parameters exclusively And all other habits use the global Workday Flow parameters And changing the global preset subsequently does not alter Habit A’s override And removing the override causes Habit A to inherit the current global preset within 60 seconds
Preset Customization, Persistence, and Reapplication
Given the user edits Morning Maker’s window from 06:00–09:00 to 05:30–08:30 local and saves And the edited preset is applied globally When the schedule is regenerated Then all non-overridden habits update to the new 05:30–08:30 window within 60 seconds And any pending nudges outside the new window are moved to the next valid slot within the new window And the customization persists across app restarts and device reboots And a versioned change entry is recorded with timestamp and editor identity
Snooze Durations & Escalation Cap
"As a busy professional, I want to snooze a reminder briefly and limit how many times it escalates so that I stay focused without losing my streak."
Description

Enables configurable quick-snooze options (e.g., 5/10/15/30 minutes) and a daily escalation cap per habit and globally. Supports an escalation ladder (subtle banner → haptic → richer push) within the cap and respects Focus/DND. Provides a global cooldown after completion to prevent over-notifying. Stores preferences in the user profile and syncs across devices for consistent behavior.

Acceptance Criteria
Quick Snooze Options in Reminder Prompt
Given a habit reminder appears with a Snooze action When the user opens the Snooze menu Then the default options 5, 10, 15, and 30 minutes are displayed And When the user has customized quick-snooze options in Nudge Blueprint Then the Snooze menu reflects the customized list instead of the defaults And When the user selects a snooze duration X minutes Then the next reminder for that habit is scheduled exactly X minutes from the selection time And no escalation for that reminder occurs before the rescheduled time And the Snooze menu closes and shows a confirmation state within the notification or app
Per-Habit Daily Escalation Cap Enforcement
Given a per-habit daily escalation cap N is configured for habit H And the current day is determined by the user's profile time zone When the app delivers escalation notifications for habit H throughout the day Then the number of escalation notifications delivered for habit H does not exceed N between 00:00 and 23:59 of that day And after N is reached, further escalations for habit H are suppressed until the next day And the cap counter resets at 00:00 in the user's profile time zone
Global Daily Escalation Cap Enforcement
Given a global daily escalation cap G is configured for the user And the current day is determined by the user's profile time zone When escalation notifications are delivered across all habits during the day Then the total number of escalation notifications delivered across all habits does not exceed G between 00:00 and 23:59 of that day And after G is reached, further escalations across all habits are suppressed until the next day And per-habit caps continue to be applied within the global limit
Escalation Ladder Ordering and Suppression
Given a habit has pending nudges and remaining headroom under both per-habit and global caps When the nudge sequence runs without user completion or snooze Then the escalation order is: subtle banner first, then haptic, then richer push And the next step is only attempted after the configured interval elapses without completion or snooze And if any step would exceed the applicable cap, that step and subsequent steps are suppressed for the day And the ladder never skips to a more intrusive step unless the preceding step was attempted and not acted upon
Focus/DND Compliance for Escalations
Given the device is in a Focus/DND mode that silences notifications When an escalation step would deliver sound or haptic Then the app does not produce sound or haptic And the step is downgraded to a silent delivery if permitted by the OS, otherwise it is skipped And suppressed or downgraded steps do not trigger multiple queued deliveries when Focus/DND ends And caps are evaluated on delivered notifications; suppressed (not delivered) steps do not increment cap counters
Post-Completion Global Cooldown Suppression
Given the user completes any habit check-in And a global cooldown duration C minutes is defined by the user's Nudge Blueprint When completion is recorded Then a cooldown timer of C minutes starts immediately And during the cooldown no escalation notifications are delivered for any habit And after the cooldown ends, escalation delivery may resume subject to per-habit and global caps And cooldown does not reset cap counters; it only suppresses delivery during the window
Preference Persistence and Cross-Device Sync
Given the user updates snooze options, per-habit caps, global cap, or cooldown in Nudge Blueprint on Device A When Device A has network connectivity Then the updated preferences are stored in the user's profile on the server within 5 seconds And when Device B comes online with the same account Then the updated preferences are applied on Device B within 60 seconds And subsequent reminder and escalation behavior on both devices reflects the updated preferences consistently And if Device A was offline during changes, the updates are synced on reconnect and resolve conflicts using last-write-wins by server timestamp And any scheduled notifications affected by the changes are recalculated within 60 seconds of sync
Streak Rescue Window & Grace Logic
"As a habit tracker, I want a final timely nudge before my streak breaks so that I can save it even on hectic days."
Description

Defines a configurable rescue window near the end of each habit’s timeframe that triggers a final, tactful nudge and applies a grace period to prevent unintentional streak breaks. Integrates with check-in logic to mark a streak saved if completion occurs within the window. De-duplicates rescue nudges when habits overlap and prioritizes higher-impact habits. Surfaces clear UI copy explaining when a streak is at risk and how rescue works.

Acceptance Criteria
Rescue Nudge Triggers Within Configurable Window
Given a habit with end time T and rescue window W minutes and the user has not checked in by time T - W When the time reaches T - W Then exactly one rescue nudge is sent using the user’s configured nudge style (silent haptic or subtle banner) Given a rescue nudge was already sent for this habit today When additional trigger conditions occur within the same window Then no additional rescue nudges are sent for that habit Given the app is in foreground at T - W When the rescue nudge triggers Then show an in-app banner aligned to the configured style instead of push/notification Given analytics are enabled When a rescue nudge is sent Then log event "rescue_nudge_sent" with habit_id, window_minutes, local_time, foreground_state
Grace Period Prevents Unintentional Streak Break
Given a grace period G minutes is configured for a habit and the habit end time is T When the user completes a valid check-in at time t where T < t ≤ T + G Then the check-in is accepted, the streak remains unbroken, and the check-in is flagged rescue_saved = true Given the user has not checked in by time T + G When the clock passes T + G by 1 second Then the streak is marked broken and the day is recorded as missed Given the user completes a check-in at time t > T + G When the system validates the check-in Then the check-in does not save the prior period’s streak and is applied to the next eligible period per habit rules (if any)
Check-in During Window Marks Streak Saved
Given a habit with end time T, rescue window W, and grace period G When the user checks in at time t where T - W ≤ t ≤ T + G Then the system marks the check-in with tag "rescue", increments the streak by 1, and prevents more than one streak increment for the habit on that calendar day Given multiple check-ins occur during [T - W, T + G] When processing subsequent check-ins Then the first check-in counts toward the streak and duplicates are ignored for streak calculation Given a rescue save occurs When the UI updates Then show a "Streak saved" confirmation on the habit card for at least 3 seconds and log event "streak_saved" with habit_id, t, window_or_grace
De-duplicate Overlapping Rescue Nudges With Priority
Given two or more habits H1..Hn qualify for a rescue nudge within the same 60-second interval When selecting which nudge to deliver Then send exactly one nudge for the highest priority habit determined by: impact_score (desc), current_streak (desc), end_time (asc), habit_id (asc) Given a rescue nudge is sent for habit Hx at time t When other habits’ rescue windows are concurrently active Then suppress additional rescue nudges for a cooldown C = 60 seconds; after C elapses, pending rescue nudges may fire if still within window and below the escalation cap Given two rescue windows overlap but start at different seconds When evaluating triggers Then enforce at most one rescue nudge across all habits per any rolling 60-second window
In-Context UI Explains At-Risk and Rescue State
Given the current time is within the rescue window [T - W, T) When the user views the habit card Then display an "At risk: ends in mm:ss" countdown and an info icon Given the current time is within the grace window (T, T + G] When the user views the habit card Then display an "In grace: save within mm:ss" countdown and an info icon Given the user taps the info icon When the modal opens Then show copy explaining rescue window, grace period, and how a rescue save works; copy fits on screen, is localized (en-US), and meets WCAG AA contrast Given the countdown is displayed When QA measures the timer Then the remaining time is accurate within ±1 second
Rescue Behavior Respects Escalation Cap and Snooze
Given a daily escalation cap E is configured and the next rescue nudge would exceed E When the rescue trigger time occurs Then do not send a push/haptic; queue an in-app banner to show on next app open that occurs before T + G Given the user taps "Snooze" on a rescue alert with snooze length S minutes When scheduling the follow-up Then schedule exactly one follow-up at min(T, now + S); if min ≤ now or outside the window, do not schedule Given a follow-up from snooze is due while other rescue nudges are pending When delivering notifications Then follow-up obeys the global de-duplication rule and escalation cap
Time Zone and DST-Safe Rescue Computation
Given the habit day is anchored to the user’s local time zone at 00:00 of that calendar day When the user changes time zone or a DST transition occurs during the day Then compute T, W, and G using the anchored zone for that day to avoid shifting the deadline unexpectedly Given a DST fall-back repeats clock times (e.g., 01:30 occurs twice) When scheduling the rescue trigger at T - W and grace end at T + G Then trigger only once at the first occurrence of each and do not duplicate due to repeated times Given a DST spring-forward skips times (e.g., clocks jump from 01:59 to 03:00) When the computed T - W falls within the missing interval Then schedule the rescue trigger at the nearest prior valid minute and still end grace based on the absolute elapsed duration relative to T
Adaptive Nudge Timing & Cooldown
"As a user who dislikes repetitive reminders, I want the app to learn when I respond best so that I get fewer but more effective nudges."
Description

Learns effective delivery moments from user behavior signals (opens, snoozes, dismissals, completion latency) to adjust nudge timing within allowed windows. Applies per-habit heuristics, extends cooldown after successful completion, and offers an opt-out or fixed-schedule mode. Reduces alert fatigue while improving conversion-to-check-in.

Acceptance Criteria
Adaptive timing selects best-performing bucket within allowed window
Given a habit with an allowed window and at least 5 prior nudge outcomes bucketed into 10-minute intervals for the same day-of-week, When scheduling the next nudge, Then the system selects the 10-minute bucket within the allowed window with the highest observed conversion-to-check-in rate, And the scheduled time is within that bucket, And a decision log records the evaluated buckets, sample sizes, rates, and the chosen time. Given fewer than 5 prior outcomes in the allowed window for that day-of-week, When scheduling the next nudge, Then the system uses the smart preset time for that day and logs fallback_reason = "insufficient_data".
Per-habit adaptive heuristics are isolated
Given a user with two habits A and B, When snoozes/dismissals/open patterns for habit A change its adaptive schedule, Then habit B’s nudge times remain determined solely by B’s own history and are unchanged by A’s signals. Given decision logs for adaptive scheduling, When inspecting logs, Then each decision is associated with a single habit_id and references only that habit’s outcome history.
Cooldown extension after successful completion suppresses further nudges
Given a user completes a check-in for a habit at time T within its allowed window, When any further nudge would be scheduled for that habit before the next allowed window starts, Then the nudge is suppressed and the next scheduled nudge is no earlier than the next allowed window start, And a cooldown_applied log entry records start=T and end=next_window_start. Given a user completes a check-in after the allowed window ends, When the scheduler runs again the same day, Then no nudges are sent for that habit until the next day’s allowed window.
Fixed schedule and opt-out modes override adaptive timing
Given a habit is set to Fixed Schedule mode with specified times, When scheduling nudges, Then nudges fire at the specified times within allowed windows, And adaptive adjustments are not applied, And decision logs label mode = "fixed". Given a habit has Nudge Opt-Out enabled, When the scheduler runs, Then no nudges are sent for that habit, And decision logs label mode = "opt_out". Given a user switches from Fixed to Adaptive mode, When the next allowed window occurs, Then adaptive timing resumes using available history for that habit.
Respect allowed windows, quiet hours/DND, snooze, and escalation cap
Given a nudge becomes due during device DND or user quiet hours overlapping an allowed window, When deferring delivery, Then the nudge is delivered at the first minute within the same window outside quiet hours, Or at the next allowed window if no time remains, And the defer reason is logged. Given a per-habit daily escalation cap C, When the Cth nudge for the habit is delivered on a calendar day, Then no additional nudges for that habit are sent that day, And attempts are logged as suppressed_due_to_cap. Given a user snoozes a nudge by S minutes and S would push delivery outside the allowed window, When rescheduling, Then the nudge is scheduled at the next allowed window start not earlier than S minutes from now, Or is canceled if Opt-Out is enabled.
Fallback to smart presets until learning threshold met
Given fewer than 5 historical nudge outcomes exist for a habit’s day-of-week window, When scheduling, Then the system uses the smart preset time aligned to the user’s blueprint schedule and logs learning_state = "preset_fallback". Given at least 5 historical outcomes exist for that day-of-week window, When scheduling, Then the system uses adaptive selection and logs learning_state = "adaptive_active". Given the user clears habit history, When scheduling thereafter, Then the system re-enters preset fallback until the threshold is reached again.
Conversion and fatigue metrics are logged and visible
Given any nudge is delivered, Then the system logs delivery_time, habit_id, nudge_style, window_id, outcome (opened/snoozed/dismissed), and whether a check-in occurred within 60 minutes and before window end. Then the habit analytics surface shows daily conversion-to-check-in rate and average snoozes per delivered nudge over the last 7 days for that habit. Given an analytics export is requested, Then raw nudge-level records for the selected date range can be exported in CSV and JSON formats.
Consent & Permissions Manager
"As a privacy-conscious user, I want transparent control over app permissions so that I can personalize nudges without compromising my privacy."
Description

Guides users through granting notification, haptic, calendar, and focus mode permissions with clear value explanations and granular toggles. Detects permission state changes, provides non-intrusive re-prompt flows, and defines fallback behaviors when access is denied (e.g., in-app banners only). Consolidates all nudge-related privacy controls under Nudge Blueprint and adheres to platform privacy policies.

Acceptance Criteria
First-Run Permission Onboarding for Nudge Blueprint
Given a first-time user enables Nudge Blueprint When they arrive at the Consent & Permissions screen Then they see four permission rows: Notifications, Haptics, Calendar, Focus Mode, each with a brief value statement and a per-feature toggle And system permission prompts are triggered only after the user enables a toggle and taps Continue And the user can proceed without enabling any permission, with clearly indicated fallback behavior And declining any system prompt leaves the corresponding toggle off and shows an "Enable in Settings" link And the entire flow can be completed in ≤ 2 taps per permission and ≤ 30 seconds median on supported devices
Real-Time Sync With OS Permission Changes
Given Nudge Blueprint is installed and the app regains foreground When the OS-level permission for Notifications, Calendar, or Focus Mode changes outside the app Then the in-app permission status updates within 3 seconds And corresponding toggles reflect the true state and become read-only with a "Manage in Settings" link if OS requires deep link And fallback nudge channels auto-adjust according to the user’s preset without interrupting the user
Non-Intrusive Re-Prompt After Denial With Throttling
Given a user previously denied Notifications permission When they return to Nudge Blueprint during a nudge-active window Then show a single non-modal banner with rationale and actions: "Enable in Settings" and "Not now" And do not re-show the banner more than once per 14 days or until 3 successful daily check-ins occur, whichever comes first And after two banner dismissals in 60 days, suppress further permission prompts for 90 days And tapping "Enable in Settings" deep-links to the correct OS settings page and returns to the app on back navigation
Fallback Nudge Behavior When Permissions Are Denied
Given Notifications are denied and Haptics allowed When a scheduled nudge fires Then deliver an in-app banner and a haptic pulse per the user’s preset And do not attempt to schedule or send notifications And if all external channels are denied (Notifications, Calendar), surface an in-app banner within 1 second of app foreground And no errors occur and the user can still complete a one-tap check-in
Unified Nudge Privacy Controls Screen
Given a user opens Settings > Nudge Blueprint > Privacy & Permissions When the screen loads Then all permission types are listed with status badges: Granted, Denied, Limited, or Not Requested And each item includes a brief use explanation, a toggle (if controllable), and a "Manage in Settings" link when applicable And toggling off a permission immediately updates stored preferences and stops using that channel for new nudges within 60 seconds And accessibility is met: controls are labeled for VoiceOver/TalkBack, contrast ratio ≥ 4.5:1, and actions are keyboard reachable
Contextual Prompting and Policy Compliance
Given the app plans to request a system permission When the user performs a related action (e.g., enabling Calendar sync or Focus-based delivery) Then show an in-app rationale that explains value and data use before any OS prompt appears And never block core habit tracking behind permissions And record consent/denial timestamps and app version for audit, exportable on user request And flows and copy conform to Apple HIG and Google Play permissions policy as verified by an internal checklist
Cross-Device Nudge Orchestration
"As a user who switches between phone and tablet, I want nudges to arrive only on my active device so that I avoid duplicate interruptions."
Description

Delivers nudges once to the right device by detecting recent activity and presence across signed-in devices. De-duplicates notifications, enforces shared escalation caps, and hands off pending nudges when the active device changes. Syncs Blueprint settings in real time and logs delivery device for analytics and support.

Acceptance Criteria
Active Device Selection and Single Delivery
Given a user is signed in on two or more devices and has a scheduled nudge at time T, and Device A had app foreground activity within the last 10 minutes while other devices did not When the nudge is triggered at T Then exactly one notification is delivered to Device A within 5 seconds And no other device receives the same nudge within a 30-minute de-duplication window or before the first delivery is acknowledged/acted on (whichever comes first) And the delivery record stores deviceId=Device A and reason="most_recent_activity"
Cross-Device De-duplication Window
Given a nudge trigger fires and a delivery is made to any device for triggerId X When another device evaluates triggerId X within the next 30 minutes or before the first delivery is acknowledged/acted on Then the subsequent delivery is suppressed And a suppression log entry is created with reason="deduplicated" and originalDeliveryId referenced
Pending Nudge Handoff on Active Device Change
Given a nudge is scheduled for time T and is currently assigned to Device A based on recent activity And at T-2 minutes the user foregrounds the app on Device B When time T is reached Then the nudge is delivered to Device B and not to Device A And the delivery log records reason="handoff", fromDeviceId=Device A, toDeviceId=Device B
Shared Escalation Cap Enforcement
Given a user’s Blueprint escalation cap is N per local day When the cumulative delivered nudges across all devices reaches N for that local day Then any further triggers that day are suppressed across all devices with status="capped" and reason recorded And the cap counter resets at the user’s local midnight per Blueprint timezone
Real-Time Blueprint Sync Across Devices
Given the user updates Nudge Blueprint settings (style, snooze lengths, escalation cap) on any device at time t0 When a nudge is triggered at or after t0+3 seconds Then all devices apply the updated settings to that delivery And each signed-in device reflects the new settings in its UI within 3 seconds of t0 And the delivery log includes blueprintVersion matching the latest settings
Cross-Device Snooze Propagation
Given a nudge is delivered to Device A and the user snoozes it for X minutes at time t0 When any device evaluates the same nudge before t0+X minutes Then the delivery is suppressed with reason="snoozed" And the next eligible evaluation time for that nudge is set to t0+X minutes across all devices And the snooze state is visible on other devices within 3 seconds
Delivery Analytics and Support Logging
Given any nudge delivery decision occurs (delivered or suppressed) When the event is recorded Then the log contains userId, nudgeId, triggerId, deviceId, deviceType, platform, appVersion, blueprintVersion, decision, reason, timestamp(UTC), escalationCount, dedupeKey, activeDeviceAttribution And the record is queryable in analytics within 60 seconds of the decision And support can retrieve the last 20 delivery decisions for the user within 2 seconds

Pass Tiers

Offer flexible pricing tiers (Drop‑In, Monthly, Season) with Stripe subscriptions and one‑time options. Creators attach clear perks to each tier—priority seating, supporter‑only prompts, extended grace windows, and distinctive badge styles—so fans choose the commitment level that fits. Timed access auto‑renews or expires cleanly, and upgrades/downgrades are one tap. Benefits: predictable creator revenue, effortless upsells, and users get transparent value for every tier without leaving StreakShare.

Requirements

Tier & Perk Model
"As a creator, I want to define and publish clear pass tiers with prices and perks so that fans can choose the commitment level that fits and I can earn predictable revenue."
Description

Define a robust data model for Pass Tiers that supports Drop‑In (one‑time), Monthly (recurring), and Season (time‑boxed recurring) types, including price, currency, duration, renewal policy, and perk entitlements (priority seating allocation, supporter‑only prompts access, extended grace window minutes/days, badge style variants). Support localization for names/descriptions, multi‑currency pricing, versioning with effective dates, and safe migrations for existing purchasers. Expose read APIs for client display and write APIs for creator tooling. Ensure referential integrity between creators, rooms, and tiers, with audit logs for changes. Outcome: a single source of truth that enables consistent display, purchase flows, and enforcement across StreakShare.

Acceptance Criteria
Create and Retrieve Tier Types and Renewal Policies
Given a creator with valid IDs and an authenticated session When they POST a tier with type=Drop-In, price amount>0, currency in ISO-4217, duration in hours/days, and renewalPolicy=one-time Then the API responds 201 with a tierId, and the stored record has type=Drop-In and renewalPolicy=one-time And the tier is linked via foreign keys to the creator (and optional room) with referential integrity enforced And a subsequent GET by tierId returns the same canonical fields and values Given a POST for type=Monthly with renewalPolicy=auto-renew and period=1 month When the request is valid Then the stored record reflects monthly recurrence with nextRenewal cadence metadata present Given a POST for type=Season with a fixed seasonLength (e.g., 12 weeks) and renewalPolicy in {auto-renew, no-auto-renew} When created Then the stored record includes season start/end (or template length) and the renewal policy Given invalid combinations (e.g., Drop-In with auto-renew, negative price, unsupported currency) When submitted Then the API responds 400 with field-level validation errors and nothing is persisted
Perk Entitlements Resolution and Enforcement
Given a tier is created with perks {prioritySeatingAllocation=10, supporterOnlyPrompts=true, graceWindowMin=15, badgeStyleVariant="gold"} When the tier is retrieved via GET Then the response includes a normalized entitlements object with these exact values and types Given a user who holds this tier When they enter a room with capacity and seating priority logic enabled Then they are placed ahead of non-priority users up to the allocation limit and this is auditable via seatingDecision metadata Given supporterOnlyPrompts=true on the tier When the client requests prompts for a session Then supporter-only prompts are included for entitled users and excluded for others (403 for non-entitled access) Given graceWindowMin is set When a user with this tier attempts a check-in late within graceWindowMin Then the system marks the check-in as on-time for streak purposes Given badgeStyleVariant is configured When the user’s profile is rendered Then the badge style matches the variant specified by the tier
Localization and Fallback for Tier Names/Descriptions
Given a tier has name/description localized for en-US and es-ES When the client requests GET /tiers/{id}?locale=es-ES Then the response returns the es-ES strings and includes locale=es-ES in metadata Given the requested locale is fr-FR and no fr-FR strings exist When the client requests the tier Then the response falls back to the default locale (e.g., en-US) and includes fallbackLocale metadata Given localized strings exceed max length or include unsafe HTML When attempting to create/update the tier Then the API responds 400 with validation errors and disallows storage of unsafe/oversized content
Multi-Currency Pricing Selection and Validation
Given a tier has prices for {USD, EUR} When a user with preferredCurrency=EUR requests pricing Then GET returns the EUR price, currency="EUR", and all available prices in a prices[] array Given a user with preferredCurrency=JPY and no JPY price exists When they request pricing Then the API returns the default currency price, sets currency="USD" (or configured default), and includes availableCurrencies metadata without performing on-the-fly conversion Given a write request includes currency codes not in ISO-4217 or non-positive amounts When submitted Then the API responds 400 and rejects the invalid price entries without creating/updating the tier
Versioning with Effective Dates and Purchase Freezing
Given a tier v1 is active and a creator schedules v2 with effectiveStart in the future When GET is called before effectiveStart Then clients receive v1 data; after effectiveStart, clients receive v2 Given a purchaser buys the tier before effectiveStart of v2 When their entitlement snapshot is created Then it references v1 and remains frozen to v1 benefits until they upgrade/downgrade or the policy dictates change Given an attempt to modify fields on a version whose effectiveStart is in the past When a PATCH is submitted Then the API rejects mutation of immutable versioned fields with 409 Conflict and instructions to create a new version Given versions v1 and v2 exist When GET /tiers/{id}/versions is called Then the API returns an ordered, complete history with effectiveStart/effectiveEnd and status flags
Safe Migrations for Existing Purchasers
Given an existing set of purchasers on tier v1 When a creator publishes v2 or migrates schema for perks Then all existing purchasers retain their v1 entitlements without interruption, and a background migration task maps references safely Given the migration job is retried due to failure When it runs again Then it is idempotent and produces the same final state without duplicate rows or lost entitlements Given any purchaser cannot be migrated due to referential errors When the job completes Then the system logs errors with actionable details and exposes counts in monitoring metrics while leaving the purchaser’s current access intact
API Contracts, Referential Integrity, and Audit Logging
Given the read API GET /tiers/{id} When called with a valid id and the caller has access Then it returns 200 with fields: id, creatorId, type, renewalPolicy, duration/period, prices[], entitlements, locale strings, currentVersion, versions[], and audit metadata Given the write API POST/PATCH with mismatched creatorId or roomId references When submitted Then the API returns 409/400 and refuses changes; database foreign keys prevent orphaned tiers; deleting a creator or room with attached tiers is blocked with 409 unless a safe cascade/archive path is used Given any create/update/delete to tiers, prices, or entitlements When the operation succeeds Then an audit log entry is written with actorId, action, entityId, before/after snapshots (redacting secrets), timestamp (UTC), and reason; GET /audit supports filtering by entityId and date range
Stripe Subscriptions & One‑Time Payments
"As a fan, I want to purchase a pass quickly and securely so that I can join rooms immediately with the perks of my chosen tier."
Description

Integrate Stripe for secure payments covering subscriptions (Monthly, Season) and one‑time Drop‑In passes. Implement product/price sync, Checkout/Payment Element flows, customer mapping, and webhooks for payment success, failure, refunds, and disputes. Support SCA/3DS, taxes, receipts/invoices, promo codes, and dunning. Enable auto‑renew toggle, cancellation, and reactivation. Persist transaction states idempotently and surface purchase status to the app in real time. Outcome: fast, compliant purchase experiences with reliable revenue capture.

Acceptance Criteria
Stripe Product and Price Sync
- Given Stripe has Products and Prices for Pass Tiers, When the scheduled sync job runs, Then StreakShare stores each Product and Price with Stripe IDs, active flags, currency, interval, and amount. - Given a Product or Price is updated in Stripe, When a related webhook is received or the next sync runs, Then the changes appear in the StreakShare admin and purchase UI within 60 seconds without creating duplicates. - Given a Product or Price is archived in Stripe, When the sync occurs, Then it is marked inactive in StreakShare and hidden from selection in checkout. - Given the same Stripe object is processed multiple times, When the sync or webhook handler runs, Then persistence is idempotent using Stripe object and event IDs.
One‑Time Drop‑In Checkout with SCA
- Given a user selects a Drop‑In pass, When they tap Buy, Then the app loads Stripe Checkout or Payment Element per configuration and displays itemized total including taxes and applied promo code before confirmation. - Given SCA/3DS is required, When the user completes authentication, Then the payment is confirmed and Drop‑In access is granted within 5 seconds of receiving the success webhook. - Given the payment is declined or authentication is abandoned, When the checkout flow ends, Then no charge is captured, access is not granted, and a clear error state is shown with a retry option. - Given a network retry occurs during PaymentIntent creation, When the client retries with the same idempotency key, Then only a single Stripe charge is created and recorded once in StreakShare.
Subscription Purchase (Monthly/Season) with Taxes and Promo Codes
- Given a user selects Monthly or Season, When they confirm purchase, Then a Stripe Customer is created or reused and a Subscription is created with the correct Price ID and billing interval. - Given taxes apply, When the user reviews the payment sheet, Then tax is calculated via Stripe and the total amount including tax is displayed prior to confirmation. - Given a valid promo code is entered, When checkout completes, Then the discount is applied per Stripe configuration and reflected on the initial invoice. - Given the first invoice is paid, When the webhook is processed, Then the subscription status is Active (or Trialing if configured), perks unlock immediately, and a receipt/invoice link is available in the app and emailed to the user. - Given the first payment fails, When the webhook indicates payment_failure, Then no perks are unlocked and the subscription reflects Incomplete/Past Due state in the app.
Auto‑Renew Toggle, Cancellation, and Reactivation
- Given a user has an active subscription, When they toggle auto‑renew off, Then the Stripe subscription is set to cancel_at_period_end=true, perks remain until period end, and the cancel-at date is shown in the app within 10 seconds. - Given a user who has set cancel_at_period_end, When they tap Reactivate before period end, Then cancel_at_period_end is cleared and renewal resumes with no interruption to perks. - Given a user whose subscription has expired, When they tap Reactivate, Then a new subscription is created with a new billing cycle and perks unlock upon payment success webhook. - Given a user requests immediate cancellation, When immediate cancel is confirmed (if offered), Then the subscription is terminated in Stripe, remaining time is not billed further, and perks are revoked immediately.
Webhook Event Processing and Idempotent State Persistence
- Given Stripe sends a payment success event (e.g., checkout.session.completed, invoice.paid, charge.succeeded), When the webhook with a valid signature is received, Then the corresponding transaction and subscription states are updated atomically and processed exactly once per event ID. - Given Stripe sends a failure event (e.g., invoice.payment_failed, charge.failed), When processed, Then the purchase shows Past Due in the app and usage restrictions are applied according to policy. - Given a refund is issued or a dispute is created, When charge.refunded or charge.dispute.created is received, Then purchase state is updated to Refunded or Disputed, access is revoked immediately, and an audit entry is recorded. - Given Stripe retries or delivers out‑of‑order events, When handlers run, Then final state reflects the latest event timestamps while remaining idempotent and thread‑safe. - Given a webhook with an invalid signature is received, When validation fails, Then the event is rejected (HTTP 400) and no persistence occurs.
Dunning and Payment Failure Handling with Real‑Time App Status
- Given a renewal payment fails, When invoice.payment_failed is received, Then the app surfaces a Past Due banner within 60 seconds, sends a prompt to update payment method, and preserves access only within the configured grace window. - Given Stripe retries and subsequently succeeds, When invoice.paid is received, Then access is restored immediately and delinquency indicators are cleared in the app within 10 seconds. - Given all dunning retries are exhausted, When the subscription transitions to Canceled/Unpaid per configuration, Then perks are revoked at grace end and the user sees a clear restart/reactivate call to action. - Given a user updates payment details from the app, When the update succeeds, Then the next retry is triggered or a payment attempt is made immediately and the app reflects the updated status in real time.
Tier Upgrade/Downgrade Proration and Benefits Application
- Given a user with an active subscription chooses Upgrade, When confirmed, Then the Stripe subscription is updated to the new Price, proration is applied per settings, any immediate charge/credit is shown and processed, and new perks activate upon invoice.paid. - Given a user chooses Downgrade, When confirmed, Then the change is scheduled for period end by default (no immediate loss of perks) and the effective date is clearly displayed before confirmation. - Given the downgrade policy is set to prorate immediately, When the change is applied, Then credits/charges are calculated by Stripe and perks reflect the lower tier immediately after the invoice event. - Given multiple rapid plan changes are initiated, When processed, Then only the latest requested state persists using idempotency keys and the billing_cycle_anchor remains consistent without double billing.
One‑Tap Tier Switching with Proration
"As a subscriber, I want to change my pass tier in one tap so that I can adjust my commitment without friction."
Description

Provide an in‑app flow to upgrade or downgrade a pass in one tap, calculating proration via Stripe and applying immediate entitlement changes for upgrades and end‑of‑period changes for downgrades. Ensure idempotent server actions, clear pricing deltas before confirmation, and instant UI updates. Handle edge cases such as pending invoices, expiring Season passes, and currency changes. Outcome: frictionless flexibility that drives upsell while minimizing support issues.

Acceptance Criteria
Instant Upgrade with Proration and Entitlements
Given a user has an active lower-priced tier subscription in the same currency with remaining time When the user taps "Upgrade" and confirms Then Stripe computes a proration credit for unused time and a charge for the upgrade effective immediately And the total due now equals Stripe's proration calculation within ±0.01 of the displayed currency And the user's entitlements (badge style, grace window, priority seating, supporter-only prompts) update in-app within 5 seconds And the subscription tier and next renewal price are updated in the billing screen and API within 5 seconds And no duplicate charges or overlapping subscriptions exist
Scheduled Downgrade at Period End with No Immediate Loss
Given a user has an active higher-priced tier subscription When the user taps "Downgrade" and confirms Then the change is scheduled for the current period end date shown pre-confirmation And no immediate charge or credit occurs; any proration/credit is applied on the next invoice And the user retains current-tier entitlements until the scheduled effective date And at the period boundary the entitlements switch to the lower tier within 5 seconds and billing reflects the new price on the next cycle And a confirmation banner and receipt are delivered within 10 seconds of scheduling
Price Delta Confirmation Modal with Currency Awareness
Given a user initiates a tier switch (upgrade or downgrade) When the confirmation modal is shown Then it displays: current tier, new tier, period alignment, taxes, credits, and total due now (or 0.00 for downgrades) in the user's billing currency And for currency changes, the modal shows the target currency, FX-converted amounts from Stripe, and the new billing currency that will apply going forward And the Confirm action is disabled until pricing data is successfully loaded from Stripe And the amounts shown match the subsequent Stripe invoice/charge within rounding rules (≤ 0.01 in the displayed currency) And Cancel cleanly aborts without any subscription change
Idempotent Server Actions and Double-Tap Protection
Given unreliable network conditions or a user double-taps Confirm within 2 seconds When the client calls the switch API with a unique idempotency key and retries occur Then the server performs at most one subscription update/invoice creation in Stripe And the user sees a single confirmation/charge and receives one receipt And the API returns the same operation id for all idempotent retries And the Confirm button transitions to a loading/disabled state within 200 ms to prevent re-submission
Pending Invoice and Payment Authentication Handling
Given the user's subscription has an open or past_due invoice or requires SCA When the user attempts a tier switch Then the flow blocks progression with a clear message and a CTA to resolve payment, or collects SCA within the same flow And after successful payment resolution, the switch resumes automatically without losing context And if resolution fails, no subscription change is made and the user is notified with next steps And the billing screen reflects the current state (blocked, resumed, or failed) within 5 seconds
Season Pass Expiry and Mid-Season Switching
Given the user holds an active Season pass with a displayed expiry date When the user switches to a subscription tier before expiry Then the new subscription is scheduled to start at the Season pass expiry by default and this start date is displayed pre-confirmation And the user may choose "Start Now" which applies proration for remaining Season value per defined policy and grants immediate entitlements And no overlapping access windows occur; access remains continuous with zero gaps And Stripe shows one active product at any moment and any credit is applied per policy
Real-Time UI, Access, and Analytics Consistency
Given a tier switch is successfully executed (immediate or scheduled) When the operation completes Then the user's badge, access to supporter-only prompts, priority seating indicator, and grace window update across app surfaces within 5 seconds And the billing history shows the new invoice or scheduled change within 5 seconds And a push/in-app receipt is sent for paid upgrades within 10 seconds And analytics events (tier_switch_initiated, tier_switch_confirmed, entitlements_applied) fire once each with consistent operation ids
Real‑Time Entitlements Enforcement
"As a room host, I want pass benefits enforced automatically so that supporters receive their perks and access is fair and predictable."
Description

Build a low‑latency entitlement service that evaluates a user’s active passes and applies benefits at runtime: allocate priority seating within room capacity rules, gate supporter‑only prompts, and extend check‑in grace windows to prevent streak decay for eligible tiers. Cache short‑lived entitlements, invalidate on purchase events, and provide deterministic fallbacks if payment state is delayed. Log decisions for auditability and expose a debugging view for creators. Outcome: accurate, real‑time perk enforcement that users trust.

Acceptance Criteria
Priority Seating Allocation with Tier Precedence
Given a room with capacity N and priority seating enabled And a defined tier order for seating precedence When users across tiers attempt to check in concurrently Then no user of a lower-priority tier is seated while any higher-priority tier user is waitlisted And total seated users never exceeds room capacity And ties within the same tier are broken deterministically by earlier check-in timestamp, then by userId ascending if timestamps are equal And the service returns a seat or waitlist decision atomically and idempotently per check-in requestId
Supporter-Only Prompt Gating by Tier Perk
Given a prompt marked supporter_only And creator-defined tier perks specify which tiers have supporter_only_prompt access When a user without the perk requests the prompt or attempts to respond Then the service returns access_denied with reason supporter_only When a user with an active pass including supporter_only_prompt access requests the prompt or attempts to respond Then the service returns access_granted And perk evaluation uses the creator’s latest published perks for the tier at the time of the request
Extended Check-In Grace Window Enforcement
Given a base grace window of X seconds for check-ins And an eligible user with an active pass that grants grace_extension_seconds = Y When the user checks in at T where T is within X + Y seconds after the cutoff Then the user’s streak is preserved and no decay is applied When a non-eligible user checks in after X seconds past the cutoff Then the user’s streak decays as per standard rules And grace_extension_seconds is taken from the user’s current entitlements at decision time
Entitlements Cache TTL and Event-Driven Invalidation
Given user entitlements are cached with a TTL of 60 seconds scoped by userId and creatorId When a purchase, upgrade, downgrade, refund, cancellation, renewal, or expiry event is received Then any cached entitlements for the affected user and creator are invalidated within 2 seconds of event receipt And the next entitlement evaluation reflects the new state without requiring app restart When the TTL elapses without an event Then the next evaluation performs a fresh read and refreshes the cache And cache keys are idempotent and consistent across app instances
Deterministic Fallbacks on Delayed Payment Signals
Given a client-initiated upgrade with no payment confirmation received within 30 seconds When evaluating entitlements Then the previous tier remains in effect and higher-tier perks are not granted with reason pending_upgrade Given a subscription renewal boundary passes with no confirmation received When evaluating entitlements within the first 10 minutes after the boundary Then the previous tier remains in effect with reason renewal_grace And after 10 minutes without confirmation the entitlements revert to free/no-pass Given a client-initiated first-time purchase with no confirmation When evaluating entitlements Then no perks are granted with reason pending_payment Given a scheduled downgrade at period end When the current period has not ended Then current higher-tier perks remain in effect with reason scheduled_downgrade
Runtime Decision Latency SLA
Given normal operating conditions and healthy dependencies When evaluating entitlements with a cache hit Then P95 latency is <= 40 ms and P99 latency is <= 80 ms measured server-side over rolling 5-minute windows When evaluating entitlements with a cache miss Then P95 latency is <= 100 ms and P99 latency is <= 180 ms measured server-side over rolling 5-minute windows And at least 99.99% of decisions complete within 300 ms And latency is measured from request receipt to decision emission, excluding client network time
Decision Logging and Creator Debug View
Given any entitlement decision (seating, prompt access, grace window) When the decision is computed Then a log entry is written within 200 ms containing decisionId, timestamp (UTC ISO 8601), requestId, userId (hashed), creatorId, roomId, feature, input passIds, resolved perks, outcome, reason codes, and latencyMs And logs are retained for 90 days and are queryable by time range, userId, creatorId, and roomId And no raw payment tokens or PII beyond hashed userId are stored Given a creator opens the Entitlements Debug view for a room and searches for a user When the user is found Then the view displays active passes (tier, start, end), resolved perks (priority seating, grace_extension_seconds, supporter_only_prompt), and effective status And the last 20 entitlement decisions with timestamps, outcomes, and reason codes are shown And updates from purchase, upgrade, downgrade, or expiry are reflected within 2 seconds of event receipt And access is restricted to the room’s creator or moderators; other users receive 403
Tier Badge System
"As a supporter, I want my tier badge to appear consistently across StreakShare so that my support and status are visible at a glance."
Description

Create distinct, accessible badge styles per tier with consistent rendering across profiles, reactions, leaderboards, and room rosters. Support dynamic theming, dark mode, and localization-safe text. Prevent impersonation with verified creator association and anti‑spoofing constraints. Provide remote configuration for limited‑time seasonal variants without app updates. Outcome: clear visual status that reinforces value and encourages upgrades.

Acceptance Criteria
Distinct, Accessible Badge Styles per Tier
Rule: For tiers Drop‑In, Monthly, and Season, each badge must use a unique combination of shape/icon/pattern not shared by any other tier; if shape and icon are identical, HSL hue difference must be ≥30°. Rule: Non-text graphical elements maintain ≥3:1 contrast against background in both light and dark themes; any text/glyph within badges maintains ≥4.5:1 contrast. Rule: Badges render crisply at 16dp, 20dp, and 24dp with correct 1x/2x/3x assets and no pixelation. Rule: Assistive tech exposes a localized label in the format “Tier badge: <Tier Name>”.
Consistent Rendering Across App Surfaces
Given a user has an active Monthly pass When the badge is rendered on profile header, reaction chip, leaderboard row, and room roster Then the visual tokens (shape, icon, fill, border, animation state) are identical across surfaces, allowing only size scaling per spec And visual regression snapshots per surface match the reference with ≤1% pixel delta And the badge does not clip, misalign, or jitter at any supported size And spacing to adjacent avatar/name respects spec (≥8dp).
Dark Mode and Dynamic Theme Compliance
Given the device toggles between light and dark mode or the app applies a dynamic theme When the theme changes Then badges re-resolve tokens without app restart and maintain ≥3:1 (non-text) and ≥4.5:1 (text) contrast And the transition completes within 200ms with no flicker or intermediate incorrect colors.
Localization-Safe Text and RTL Support
Given the device locale is set to de, fr, es, ja, zh-Hans, ar, and a pseudolocale When badges with optional text labels render Then localized labels fit within bounds at minimum size or truncate with ellipsis without overlap And in RTL locales the badge placement mirrors per spec and does not overlap names/avatars And assistive labels are localized with no tofu/missing glyphs.
Verified Creator Association and Anti-Spoofing
Given a user is subscribed to Creator X’s Monthly tier and posts in Creator X’s room When the badge renders Then creator-linked styling displays only if the server response includes a valid signature and unexpired timestamp And the same user posting in other creators’ rooms does not display Creator X–specific styling And attempts to render a tier badge without server authorization are rejected and logged And the badge slot is a dedicated UI element separate from user-editable name/avatar content to prevent spoofing.
Remote-Configured Seasonal Variants (No App Update)
Given a seasonal variant for the Monthly tier is published via remote config with version, start/end, and signature When the client receives or refreshes config Then eligible users see the variant within 60 seconds or on next app foreground without updating the app And on invalid signature, expired window, or fetch failure, the client falls back to the default style without crash And assets are cached with TTL and purged after expiry And rollback to default propagates within 60 seconds.
Real-Time Badge Refresh on Upgrade/Downgrade/Expiry
Given a user upgrades, downgrades, or their pass expires or enters a grace window When the entitlement change event is received or entitlements are refreshed Then the badge state updates across all app surfaces within 5 seconds or on next app open if offline And grace-window styling appears only during the configured window and then reverts/removes And no stale tier badge persists after expiry.
Auto‑Renewal & Expiry Lifecycle
"As a subscriber, I want my pass to renew or end cleanly with clear notifications so that I’m never surprised by access changes."
Description

Implement lifecycle handling for renewals and expiries: pre‑renewal reminders, upcoming‑charge disclosures, failed payment retries with dunning, extended grace windows by tier, clean expiry with immediate entitlement removal, and reinstatement upon recovery. Ensure timezone‑aware scheduling, deterministic state transitions via webhooks, and user notifications in‑app and via email/push. Outcome: predictable access changes that reduce churn and support load.

Acceptance Criteria
Pre-Renewal Reminder & Upcoming Charge Disclosure
Given a user has an active Monthly or Season pass with auto-renew enabled and a valid default payment method When the next renewal is 72 hours away in the user’s profile timezone Then send a pre-renewal notification via in-app, email, and push including amount, currency, renewal date/time (localized), payment method brand/last4, and a Manage Pass link, and log delivery once per channel Given the same pass is 24 hours from renewal in the user’s profile timezone When a prior 72-hour reminder was sent Then send a single 24-hour reminder per channel and do not duplicate if already delivered; record delivery in an audit log Given a pass is set to cancel at period end or auto-renew is off When within 72 or 24 hours of period end Then do not send pre-renewal charge reminders and instead show an in-app expiration banner only Given a Drop‑In (one-time) pass When approaching end of access Then do not send upcoming-charge reminders (no charge occurs)
Failed Payment Retries & Dunning Notifications
Given a renewal attempt fails and Stripe sends invoice.payment_failed for the current invoice When the event is processed Then set subscription state to PastDue, schedule retries at T+24h, T+72h, and T+120h, and send a dunning notification via in-app, email, and push with the failure reason (if provided), next retry time, and Manage Payment link Given a user updates their default payment method before the next scheduled retry When the next retry executes Then attempt the charge on the new method, cancel previously scheduled later retries upon success, and stop further dunning messages Given multiple invoice.payment_failed webhooks are delivered for the same invoice When processing subsequent duplicates Then do not send duplicate dunning notifications or create duplicate retry schedules (idempotent by event and invoice id) Given all retries are exhausted without success When the last retry fails Then transition the subscription to Unpaid and expose a visible grace countdown (if applicable by tier)
Grace Window Enforcement by Tier
Given a subscription enters PastDue or Unpaid after a failed renewal When determining access Then apply grace windows by tier: Monthly = 72 hours, Season = 168 hours, Drop‑In = 0 hours, unless the tier configuration defines a custom grace_window_hours, in which case use that value Given a user is within an active grace window When accessing creator perks Then maintain all entitlements as before (priority seating, supporter‑only prompts, badge style) and display an in‑app banner with a live countdown; API returns grace_end_at in ISO‑8601 Given the grace window elapses When current time is greater than grace_end_at Then immediately end grace (trigger expiry handling) and prevent further access to supporter entitlements
Clean Expiry & Immediate Entitlement Removal
Given a pass reaches the end of its paid period with auto‑renew off, or a subscription’s grace window has elapsed When current time crosses the expiry threshold Then revoke all supporter entitlements within 60 seconds, update badge styling to non‑supporter, remove priority seating, and hide supporter‑only prompts across clients without requiring logout Given entitlement revocation occurs When notifying the user Then deliver a single expiration notification via in‑app, email, and push with clear recovery instructions and log the notifications Given the subscription is expired When updating system records Then set internal status to Expired with ended_at timestamp, stop any further charge attempts, and emit a real‑time entitlement_revoked event to active sessions
Reinstatement Upon Recovery
Given a subscription is in PastDue or Expired status for the latest period When Stripe sends invoice.payment_succeeded or customer.subscription.updated indicating Active with a new current_period_end for the same subscription Then restore all entitlements within 60 seconds, remove dunning/expiry banners, and send a recovery confirmation via in‑app, email, and push Given recovery occurs after an expiry event When entitlements are restored Then use Stripe’s current_period_end for the renewed access window, do not create a new subscription record, and cancel any scheduled expiry follow‑ups Given duplicate payment_succeeded webhooks arrive When processing subsequent duplicates Then do not create duplicate notifications or reapply entitlements (idempotent by event and invoice id)
Timezone-Aware Scheduling & DST Resilience
Given a user’s profile timezone is set to a valid IANA identifier When scheduling pre‑renewal reminders, retries display times, and grace windows Then compute triggers using the user’s timezone for wall‑clock targeting and store canonical UTC timestamps with the associated timezone Given the user changes their profile timezone When pending reminders exist Then reschedule future reminders within 5 minutes to the equivalent new local times, without sending duplicates, and update displayed times immediately Given a DST transition alters local clock time When reminders cross the transition Then preserve intended wall‑clock times (e.g., 09:00 local) without skipped or duplicated sends and maintain correct grace window durations in absolute time
Webhook-Driven Deterministic State Transitions & Idempotency
Given Stripe webhook events (invoice.upcoming, invoice.payment_succeeded, invoice.payment_failed, customer.subscription.updated, customer.subscription.deleted) may arrive out of order or be retried When processing events Then apply a deterministic state machine keyed by subscription and invoice ids, ignore stale transitions using event creation time and invoice sequence, and ensure idempotent side effects by de‑duplicating on Stripe event id Given webhook processing encounters a transient failure When retries occur Then reprocess with exponential backoff and move to a dead‑letter queue after N attempts with alerting, without leaving the subscription in a partial state Given concurrent events target the same subscription When they are processed in parallel Then serialize state updates to prevent race conditions and produce a single authoritative subscription state with complete audit logs of before/after and trigger source
To Do
Creator Tier Configuration
"As a creator, I want an in‑app dashboard to configure tiers and perks so that I can launch and iterate without developer help."
Description

Deliver an in‑app dashboard for creators to create, edit, and publish tiers: set prices and currencies, attach perks, configure badge styles, choose eligible rooms, preview purchase flows, and schedule changes with versioning. Include validation (e.g., capacity rules for priority seating, grace window limits), draft/publish workflow, and rollback of misconfigurations. Surface key metrics (active passes, MRR, churn) to inform adjustments. Outcome: self‑serve control that enables rapid iteration and transparent value communication.

Acceptance Criteria
Create and Save Tier Draft
Given a creator with manage-tiers permission is on the Tier Configuration dashboard When they click "New Tier" and enter a unique tier name, price, currency, billing type (one-time/Monthly/Season), at least one perk, a badge style, and select eligible rooms Then the "Save Draft" action becomes enabled And on save the server persists a Draft version and returns HTTP 200 with a version id And the draft appears in the tiers list with status "Draft" and a last-edited timestamp And the draft is not visible in fan purchase surfaces or room perk gating And autosave stores changes within 2 seconds of field edits without data loss on reload And the currency list is limited to Stripe-supported currencies for the creator account, defaulting to the account’s default currency
Validation Rules: Pricing, Perks, Capacity, Grace Window
Given the creator enters pricing and perk settings for a tier When price is below the system minimum for the chosen currency or not aligned to the currency’s minor unit Then the form blocks save/publish and shows an inline error with code E_PRICE_INVALID Given the tier includes a Priority Seating perk and at least one eligible room When capacity entered exceeds the room capacity or the sum of all tiers’ priority capacities for a room would exceed that room’s capacity Then publish is blocked with inline error E_CAPACITY_EXCEEDED identifying offending rooms Given the tier includes a Grace Window perk When the grace window exceeds the system-configured maximum (e.g., 30 minutes) or is negative Then the form blocks save/publish and shows error E_GRACE_LIMIT And all validation errors are mirrored server-side and return HTTP 422 with structured fields and localized user-friendly messages
Draft to Publish with Scheduling and Versioning
Given a valid Draft version exists When the creator selects Publish Now Then a new Published version v(N+1) is created and becomes active immediately, and fans see the new tier in purchase surfaces within 60 seconds Given a valid Draft version exists and the creator selects Schedule Publish with an effective datetime in their timezone When the datetime is in the past or conflicts with an existing scheduled version for the same tier Then scheduling is blocked with error E_SCHEDULE_CONFLICT When a schedule is set Then the scheduled version can be edited or canceled until the effective time And at the effective time the change applies atomically with audit log of actor, timestamp, and diff And attempts to publish/schedule an invalid configuration are rejected with HTTP 422
Rollback to Prior Published Version
Given a tier has at least one prior Published version When the creator selects Rollback to that version and confirms Then the system creates a new Published version v(N+1) that matches the selected prior configuration and activates it And fans see the reverted perks/pricing within 60 seconds And an audit record captures actor, timestamp, target version, and reason And current subscribers’ entitlements are preserved for the current billing period per subscription settings (no mid-period removal of already-granted perks) And rollback is blocked if the prior version violates current validation rules, with error E_ROLLBACK_INVALID
Purchase Flow Preview (Test Mode)
Given a tier draft or published version passes validation When the creator clicks Preview Purchase Then a test-mode checkout simulating the Stripe flow opens with the tier’s perks, price, currency, and tax settings And completing the flow leads to a test-only success screen and test email/notification, without creating live Stripe charges or customers And if required fields are missing, the preview button is disabled with a tooltip indicating missing fields And the preview reflects the selected locale and formats currency correctly for that locale
Eligible Rooms Scoping and Perk Propagation
Given the creator edits Eligible Rooms for a tier When they select specific rooms and save Then the tier’s perks (e.g., supporter-only prompts, priority seating, badge style) apply only in those rooms And in-room UI surfaces the tier badge and perk gating to eligible fans within 2 minutes of save When a room is removed from eligibility Then perk gating and visuals are removed from that room within 2 minutes And priority seating capacity is enforced per-room and tracked independently across rooms And the room selector supports search and pagination for creators with >50 rooms
Metrics Dashboard: Active Passes, MRR, Churn
Given the creator opens the Metrics section of Tier Configuration When the page loads Then it displays per-tier and aggregate: Active Passes count, MRR, and Churn (count and rate), with a visible Last Updated timestamp And metrics reconcile with Stripe within ±1% for MRR and exact counts for Active Passes/Churn over the same window, or show a warning state if reconciliation fails And filters allow selection by tier, time window (7/30/90 days), and currency, and results update within 2 seconds after selection And CSV export produces a file with selected filters applied and column headers documented And only creator admins can view metrics; others receive HTTP 403 and see an access-denied message

Drop‑In Pass

A frictionless, one‑time pass for a single session or day. Apple Pay/Google Pay completes in seconds, then Auto‑Room Link deep‑opens to the right room with a preselected micro‑commitment for instant check‑in. Access is timeboxed to the live session and rolling window, with SafeTap Undo and pro‑rated protection if the room shifts. Benefits: perfect for first‑timers and busy schedules—try before subscribing while never risking streak integrity.

Requirements

One-Tap Wallet Payment
"As a first-time visitor, I want to buy a Drop‑In Pass with Apple Pay/Google Pay in one tap so that I can join a session immediately without creating an account or entering card details."
Description

Integrate Apple Pay and Google Pay to enable a frictionless one-time Drop‑In Pass purchase with a single confirmation. The payment sheet must display localized pricing, taxes/fees, and merchant branding, support production and sandbox environments, and comply with PCI via a certified PSP. Implement success/failure/cancel callbacks, idempotent charge handling, and receipt generation. Provide admin-configurable price tiers, currencies, and regional availability. Support 3DS/SCA where required, robust error states with retry, and a refund/partial-refund pipeline to support pro‑rated protection. Expose purchase events to analytics for funnel tracking and attribution.

Acceptance Criteria
Localized Wallet Sheet Display & Merchant Branding
- Given the user’s device locale is en-GB and region is UK with an active GBP price tier, When the user taps “Buy Drop‑In Pass”, Then the wallet sheet displays the total in GBP with the £ symbol and correct ISO code, and the amount matches the configured tier. - Given taxes/fees are configured for the user’s region, When the wallet sheet is presented, Then taxes/fees appear as separate labeled line items using localized number formats. - Given merchant branding is configured, When the wallet sheet opens, Then the merchant display name matches configuration and the brand icon appears per Apple Pay/Google Pay guidelines. - Given the user’s region is not enabled, When the user taps “Buy”, Then the wallet sheet does not open and a region‑not‑available message is shown.
Environment Switching & PSP/PCI Compliance
- Given the app environment is Sandbox, When the wallet sheet opens, Then the PSP sandbox keys and test merchant IDs are used, test cards are accepted, transactions are marked as test, and no live charges occur. - Given the app environment is Production, When the wallet sheet opens, Then PSP live keys and merchant IDs are used, and a live charge is created upon authorization. - Given client and server telemetry, When a payment is executed, Then no PAN/CVV is collected or stored by app servers, and only PSP‑provided payment tokens are transmitted to the certified PSP endpoints.
Success, Failure, and Cancel Callbacks with UI Outcomes
- Given the user authorizes payment, When the PSP confirms success, Then onSuccess fires within 2 seconds, purchase state becomes “Paid”, a unique receipt ID is generated, and the UI advances to the post‑purchase state. - Given the PSP returns a decline or processing error, When failure occurs, Then onFailure returns a standardized error code and message, presents a retry CTA for transient errors, and no charge or receipt is recorded. - Given the user closes the wallet sheet without authorizing, When cancel occurs, Then onCancel fires and the user remains on the purchase screen with no charge created.
Idempotent Charge Handling and Retries
- Given duplicate client requests or repeated PSP webhooks with the same idempotency key, When processing the purchase, Then only one charge is created and subsequent attempts return the original purchase result. - Given a network timeout or 5xx error, When the user taps Retry, Then the same idempotency key is reused for up to 3 attempts within a 10‑minute window. - Given rapid consecutive taps on “Buy”, When concurrent requests arrive, Then server‑side locking prevents duplicate charges and returns a single purchase_id.
3DS/SCA Challenge Flow
- Given the issuer requires SCA, When the user initiates payment, Then a 3DS challenge is presented via the wallet per scheme rules without leaving the app. - Given the user successfully completes the challenge, When the PSP notifies success, Then the payment transitions to “Paid” without additional user action. - Given the user fails or times out the challenge, When the PSP notifies failure, Then the flow returns to the purchase screen with a clear error message and no charge captured.
Refund and Partial‑Refund Pipeline (Pro‑Rated Protection)
- Given a room shift reduces usable access, When a pro‑rated refund is calculated, Then the refund equals the unused access value rounded to the nearest cent and a partial refund is created via the PSP within 2 minutes. - Given an admin initiates a full refund, When processed, Then the original charge is fully reversed, a refund receipt is issued, and the purchase state becomes “Refunded”. - Given a refund is executed, When analytics events are emitted, Then purchase_refunded includes refund_id, original_charge_id, amount, reason, and environment.
Analytics and Attribution for Purchase Funnel
- Given a user progresses through the funnel, When key steps occur, Then events are emitted: pay_sheet_presented, pay_authorized, pay_succeeded, pay_failed, pay_canceled, refund_initiated, refund_succeeded, each with properties user_id (hashed), env, wallet_type, locale, currency, amount, taxes, region, 3ds_required (bool), attempt_count, and idempotency_key (hashed). - Given duplicate callbacks or retries, When events are emitted, Then a stable purchase_id is included so downstream deduplication yields one successful conversion per purchase. - Given an event is emitted, When data is ingested, Then it is available in analytics within 5 minutes for at least 95% of events.
Auto‑Room Deep Open & Preselected Micro‑Commitment
"As a busy creator, I want to be deep‑linked into the exact live room with my micro‑commitment preselected so that I can check in instantly after paying without navigating menus."
Description

After payment success, deep-open directly into the target room’s live session using a signed Auto‑Room Link that includes the preselected micro‑commitment and session metadata. If the native app is not installed, fall back to a mobile web experience with the same prefill and a prompt to install. Ensure secure token validation, single-use link consumption, and expiration aligned to the session window. Prefill the check‑in UI for instant confirmation, and preserve attribution (UTM/referrer) through the handoff. Handle edge cases such as stale links, device switch, and revoked room access gracefully.

Acceptance Criteria
Native Deep Open with Prefilled Micro‑Commitment After Payment
Given a successful Drop‑In Pass payment and the StreakShare app is installed When the Auto‑Room Link is invoked within the allowed session window Then the native app opens directly to the target room’s live session within 2 seconds And the check‑in UI is prefilled with the purchased micro‑commitment and session metadata (roomId, sessionId, startAt, endAt) And the user can confirm check‑in with a single tap, recording the event with correct identifiers on server and client And no intermediate screens (paywall or manual room selection) are shown
Mobile Web Fallback with Prefill and Install Prompt
Given the StreakShare app is not installed or native deep‑open fails When the Auto‑Room Link is invoked Then the mobile web experience loads within 3 seconds with the same prefilled micro‑commitment and session metadata And a persistent install prompt is displayed without blocking check‑in And confirming check‑in on web records the event with the same identifiers as native
Signed Token Validation and Single‑Use Consumption
Given an Auto‑Room Link containing a signed, single‑use token When the token is redeemed for the first time Then the server validates signature, audience, roomId, sessionId, and expiration and marks the token consumed And subsequent redemption attempts return a non‑success response (HTTP 410/409) and display “Link already used” in client without granting access And any tampered or malformed token is rejected (HTTP 401/403) without revealing room details
Expiration Aligned to Session Window
Given a configured session window with a rolling grace period When the link is invoked inside the window Then access is granted and check‑in is allowed When the link is invoked outside the window Then access is denied with “Pass expired” messaging and CTAs to browse active rooms And the displayed expiration time matches server configuration to the minute
Attribution Preservation Through Payment and Deep‑Open
Given UTM and referrer parameters captured at payment When the Auto‑Room Link deep‑opens to app or web Then those parameters are preserved and attached to the pass redemption and check‑in analytics/events And the data is queryable in analytics with the same passId and sessionId across payment, redemption, and check‑in
Edge Cases: Stale Link, Device Switch, Revoked Room Access
Given a stale link due to session cancelation or token expiry When the user invokes the link Then a friendly error is shown with CTAs to find another session; no check‑in is recorded Given the link was redeemed on device A When the same link is opened on device B Then access is denied with “Already used” messaging and guidance to acquire a new pass Given the room owner has revoked access or made the room private When the link is invoked Then access is denied without exposing private room content and a support/contact CTA is shown
Prefill Integrity and UI Consistency
Given a preselected micro‑commitment was purchased When the deep‑open renders the check‑in UI Then the micro‑commitment text and identifier exactly match the purchased option And session metadata shown in UI matches the link payload and server data And any mismatch blocks confirmation and logs an integrity error
Timeboxed Access Enforcement & Rolling Window
"As a drop‑in guest, I want my pass to work only during the live session and grace period so that access matches the event timing without lingering permissions."
Description

Gate Drop‑In Pass access to the live session and a configurable rolling window (e.g., X minutes before/after start). Enforce on the server with authoritative time checks, timezone/DST awareness, and replay protection. Display countdown timers and stateful CTAs (Join, Re‑enter, Expired). Allow re‑entry within the valid window and hard-lock after expiry. Provide admin settings for window durations and per-room overrides. Emit state transitions to analytics and logs for auditability. Ensure consistency across devices and network interruptions.

Acceptance Criteria
Server-Authoritative Timebox with TZ/DST
Given a room scheduled at T_room in timezone TZ with DST rules When a Drop‑In Pass holder requests access Then the server computes window_open = T_room − pre_window and window_close = T_room + post_window using TZ/DST-aware conversion and server time Given a client device has incorrect local time or timezone When it requests access Then access state is determined solely by server time and TZ, and the response includes canonical server timestamps (window_open_at, window_close_at) Given the current server time is before window_open When the user requests access Then the server returns 403 with state=open_in and seconds_until_window_open > 0 Given the server time is within [window_open, window_close] When the user requests access Then the server returns 200 with state=joinable and window_close_at Given the server time is after window_close When the user requests access Then the server returns 403 with state=expired and window_closed_at
Rolling Window Admin Configuration & Overrides
Given default pre_window_minutes and post_window_minutes exist at the app level When a new room is created without overrides Then those defaults apply and are returned in room metadata and used for enforcement Given a room has per-room override values set by an authorized admin When fetching the room config and enforcing access Then the override values supersede app defaults Given an admin updates window durations to valid values within [0, 180] minutes When enforcement occurs after propagation (<= 60 seconds) Then the new values apply and a config_version is included in logs Given invalid values (negative, > 180, non-integer) are submitted When saving the configuration Then validation fails with 422 and specific error codes (invalid_range, invalid_type) Given a Drop‑In Pass was issued before a config change When enforcing access after the change Then the latest room config is applied (not values at issuance) and the decision is logged with the applied config_version
Stateful CTAs and Countdown Timers
Given state=open_in with seconds_until_window_open > 0 When the room screen is visible Then the CTA label is "Opens in mm:ss", the button is disabled, and the countdown updates every 1s reaching 00:00 exactly at window_open Given state=joinable and session_not_started When the user is within the window Then the CTA label is "Join", the button is enabled Given state=joinable and the user has already joined during the session When they return within the window Then the CTA label is "Re-enter", the button is enabled Given state=expired When rendering the room screen Then the CTA label is "Expired", the button is disabled, and no check-in action can be initiated Given a state transition occurs (open_in → joinable → expired) When the device is online Then the UI updates within 2 seconds of server response and uses server-provided timestamps to drive countdowns
Re-entry Within Window and Hard Lock After Expiry
Given a user joined with a valid Drop‑In Pass within the access window When they disconnect or close the app within [window_open, window_close] Then they can re-enter using the same pass without additional charge and retain eligibility Given the server time is after window_close When the user attempts re-entry with the same pass Then access is denied with state=expired and reason=window_closed Given a network interruption occurs during join When the user retries within the window Then the join endpoint is idempotent and returns the same session_id and pass_id Given the user re-enters multiple times within the window When analytics are emitted Then exactly one joined event exists and subsequent attempts are recorded as re_enter events
Replay Protection and Token Security
Given a Drop‑In Pass token is issued When it is used to join Then the server binds it to pass_id + room_id + user_id, advances a nonce, and sets token_expires_at = window_close Given the same token is replayed from another device or IP without a valid re-entry When validated within the window Then the server rejects with 401 reason=replay_detected and logs the attempt Given the token is presented after window_close When validating access Then the server rejects with 403 state=expired reason=window_closed Given requests arrive out of order When validating Then nonce ordering and token expiry are enforced to prevent race-condition admission Given network transmission of the token When requests are made Then the token is only sent over HTTPS and a fresh nonce/token is issued on each successful join/re-entry
Analytics and Audit Logs for State Transitions
Given a Drop‑In Pass lifecycle When state transitions occur (open_in, joinable, joined, re_enter, expired, access_denied) Then analytics events are emitted with event_name, room_id, user_id, pass_id, previous_state, new_state, and server_timestamp (ISO 8601 UTC) Given at-least-once delivery semantics When duplicate events are received downstream Then each event includes idempotency_key to enable deduplication Given audit logging is enabled When access is denied Then a log entry is written with reason_code, server_time, client_time, client_tz, device_id, and request_id Given a room's window config changes When the change is saved Then a config_changed event is emitted with old_value, new_value, actor_id, and config_version
Cross-Device Consistency and Network Interruptions Resilience
Given the same user opens the room on two devices When each requests access state Then both receive the same canonical state based on server time within 500 ms of each other Given the device goes offline When the window state cannot be confirmed Then the app displays the last known server state with an Offline badge and requires server revalidation before enabling Join Given the device clock is skewed by more than 5 minutes When rendering countdowns Then the app uses server-provided timestamps to drive timers and shows a clock skew warning Given the app returns from background within the window When resuming Then the app refreshes state from the server within 1 second and adjusts the countdown without a visible jump greater than 1 second
Pass Issuance, Redemption, and Single‑Use Security
"As a product owner, I want each pass to be securely issued and redeemed exactly once for the intended session so that access is fair and resistant to fraud."
Description

Create a Drop‑In Pass domain model with lifecycle states (issued, redeemed, re‑entry, expired, refunded) and a signed, single‑use token bound to user or guest identity and target session. Prevent reuse across sessions and enforce one active redemption at a time. Implement server-side validation, rate limiting, anti-replay nonces, and device fingerprint correlation to deter abuse. Provide admin/pass lookup tools, audit logs, and webhook events for external monitoring. Expose a redemption status API to power UI badges and entry gates.

Acceptance Criteria
Single-Use Pass Issuance with Token Binding
Given a successful Apple Pay or Google Pay authorization When the server completes payment verification Then a Drop‑In Pass is created with state=issued and a unique pass_id And a signed, short‑lived token (JWS) is generated containing pass_id, subject_id (user_id or guest_id), target_session_id, iat, exp (<= 10 minutes), nonce (>=128 bits), and device_fingerprint_hash And the token signature verifies against the platform’s published JWK set And the token hash (SHA‑256) is stored server‑side for lookup and audit And an audit log entry is recorded with reason=issued and actor=system And the pass is not redeemable before the session’s redemption window_open timestamp
Redemption with Auto‑Room Deep Link and Preselected Micro‑Commitment
Given a valid issued pass and token within its exp And the current time is within the session redemption window When the user taps the Auto‑Room Link Then the app deep‑opens to the target session room And the micro‑commitment specified in the pass is preselected And server‑side validation consumes the token and sets pass state=redeemed And the response includes room_join_url and check‑in_ready=true And the token cannot be used again after this response is issued And an audit log entry is recorded with reason=redeemed and context=device_fingerprint_hash
Re‑Entry Within Timeboxed Window
Given a pass in redeemed state and the session is still within the allowed live/rolling window When the same subject requests re‑entry from a device with matching device_fingerprint_hash Then access is granted without issuing a new pass and the pass state transitions to re‑entry And no additional charges are incurred and the streak integrity flag remains true And an audit log entry is recorded with reason=re‑entry When the subject requests re‑entry from a device with non‑matching device_fingerprint_hash Then the request is denied with 403 and reason=device_mismatch and no state change occurs
Prevent Reuse, Cross‑Session Replay, and Concurrency
Given any attempt to redeem a token that has already been consumed When the server detects prior consumption via nonce or token_hash Then the request is rejected with 409 and reason=replay_detected and no access is granted Given a token bound to target_session_id=A When it is presented for session_id=B Then the request is rejected with 403 and reason=session_mismatch Given a user or guest with an active redeemed or re‑entry pass When a concurrent redemption is attempted for any session Then the request is rejected with 409 and reason=active_redemption_exists And all denials are recorded in audit logs with client_ip and device_fingerprint_hash
Server Validation, Rate Limiting, and Anti‑Replay Controls
Given a redemption request When the server validates the token signature, exp, iat, nonce uniqueness, subject binding, session binding, and redemption window Then the request proceeds only if all validations pass Given excessive redemption attempts detected (e.g., >5 attempts per minute per token_hash+IP or >20 per hour per subject) When limits are exceeded Then the server returns 429 with a Retry‑After header and records reason=rate_limited And anti‑replay storage persists used nonces until token exp+5 minutes TTL And validation failures do not disclose which check failed beyond standardized reason codes
Lifecycle Expiry, Refunds, and Room Shift Protection
Given the session live window has ended and no refund was issued When the current time is past window_close Then the pass transitions to expired and cannot be redeemed or re‑entered And an audit log entry is recorded with reason=expired Given the host shifts the session start time after pass issuance When the shift is within the policy threshold Then the pass remains valid for the rescheduled window without user action When the shift exceeds the policy threshold or the user cannot attend Then the pass can be refunded, transitioning state=refunded and disabling future redemption And refund and shift events are captured in audit logs with prior and new schedule metadata
Admin Tools, Webhooks, and Redemption Status API
Given an admin user with proper permissions When they search by pass_id, token_hash, subject_id, or session_id Then the admin UI returns pass records with current state and full state transition history And audit logs display timestamp, actor, reason_code, request_id, client_ip, and device_fingerprint_hash And webhook events are emitted for pass.issued, pass.redeemed, pass.reentry, pass.expired, pass.refunded, and pass.redemption_failed with signed payloads and retries on 5xx Given a client queries GET /v1/passes/{pass_id}/status as the owning subject When the pass exists Then the API returns 200 with state, session_id, window_open, window_close, redeemable, reason_code, and updated_at And returns 403 if requester is not the owner, 404 if not found And state changes are reflected by the API within 1 second of the change
SafeTap Undo & Pro‑Rated Protection
"As a cautious first‑timer, I want an easy undo and fair compensation if the session shifts so that I feel safe trying a drop‑in without risking my money or streak."
Description

Add an undo window after check‑in to reverse accidental taps without harming streak integrity. If a room reschedules or ends early, automatically pro‑rate via partial refund or credit issuance based on configurable rules. Use event-driven triggers (schedule change, cancellation, outage) to compute eligibility and execute payouts via PSP APIs. Notify users in‑app and by email/push, and reflect changes in receipts and pass history. Maintain a transparent ledger for disputes and CS resolution.

Acceptance Criteria
SafeTap Undo within Configured Window
Given a user completes a check-in with a Drop-In Pass And UndoWindowSeconds is configured in settings When the user taps SafeTap Undo within UndoWindowSeconds Then the check-in status reverts to "not checked in" and the Undo CTA is removed And the user's streak value returns to its pre-check-in state with no penalty or decay And the Drop-In Pass remains valid for the session’s remaining access window And an analytics/event log "checkin_undo" is recorded with user_id, pass_id, room_id, timestamp And further Undo attempts after UndoWindowSeconds result in no state change and an inline message "Undo window expired" And only one undo action is permitted per check-in event
Pro-Rated Refund for Early Session End
Given a room's scheduled duration and price, and an actual end time earlier than scheduled And ProRateRules exist with grace_minutes, min_refund_percent, min_payout_amount_cents, and payout_method When the session ends earlier by more than grace_minutes Then pro_rated_amount = price * (unused_minutes / scheduled_minutes), rounded to the nearest cent And if pro_rated_amount / price < min_refund_percent, set pro_rated_amount = 0 And if pro_rated_amount < min_payout_amount_cents, set pro_rated_amount = 0 And if pro_rated_amount > 0 and payout_method = "refund", submit a partial refund via PSP; else issue in-app credit of pro_rated_amount And the payout is initiated within 60 seconds of the "session_ended" event And the pass history and receipt are updated to reflect the adjustment
Event-Driven Pro-Rating on Schedule Changes, Cancellation, or Outage
Given a purchased Drop-In Pass for a scheduled room And an event of type "schedule_changed", "canceled", or "outage" is received And RuleConfig defines eligibility, ShiftGraceMinutes, and auto_transfer preference When the event is processed Then the system determines eligibility using RuleConfig and session timings And if eligible and auto_transfer = true, the pass is transferred to the new slot and the user is notified And if eligible and auto_transfer = false or the user declines, compute and issue pro-rated credit/refund per rules And if the event prevents attendance within the allowed window, mark the day as "protected" so the user's streak does not decay And computation completes and any payout is initiated within 60 seconds of event receipt And the operation is idempotent for duplicate events of the same type for the same pass
PSP Refund Execution and Idempotency
Given an eligible partial refund to a payment service provider When initiating the refund Then an idempotency key composed of pass_id + event_type + event_id is used And the request includes amount, currency, original charge_id, reason, and metadata mapping to ledger_id And on transient failure or timeout, the request is retried up to 3 times with exponential backoff And duplicate webhook/event deliveries do not create duplicate refunds And the PSP response reference_id is stored and linked to the pass adjustment And the refund status is tracked until terminal state "succeeded" or "failed", with the user notified accordingly
User Notifications, Receipts, and Pass History Updates
Given any successful Undo, credit issuance, or refund initiation When the action occurs Then an in-app notice is shown immediately with amount (if any), reason, and next steps And a push notification and email are sent within 2 minutes containing amount, reason, and reference_id And the receipt is updated to include an adjustment line with type (refund|credit), amount, timestamp, and rule_version And the pass history shows a new entry with state (undone|credited|refunded), amounts, and links to receipt and ledger And localization strings are used for the user's language and currency formatting matches the user's locale
Transparent Ledger and Dispute Support
Given any state change or monetary adjustment affecting a Drop-In Pass When the change is committed Then an immutable ledger entry is appended with fields: ledger_id, user_id, pass_id, room_id, event_type, rule_version, amount, currency, balance_impact, PSP_ref, idempotency_key, timestamp, actor, rationale And ledger entries are write-once and cannot be edited; corrections are recorded as compensating entries And support/admin can retrieve ledger and pass history by pass_id within 200 ms for the 95th percentile And the ledger record is visible to the user in-app with plain-language reason and amounts And exports (CSV/JSON) are available for customer support within one click, redacting PII per policy
Guest Flow & Lightweight Account Creation
"As a first-time user, I want to join as a guest using just my email or phone so that I can try a session quickly without committing to full signup."
Description

Enable pass purchase and redemption without a full account by supporting an email or phone-based guest profile. Collect minimal consented data, verify contact via OTP or magic link, and bind the pass to this identity for re-entry and receipts. Provide seamless upgrade-to-account after the session, preserving history and any credits. Handle device changes by re-authenticating via the verified contact. Implement privacy, data retention, and deletion workflows for guest data in compliance with regional laws.

Acceptance Criteria
Guest Pass Purchase with OTP Verification and Auto‑Room Deep Link
Given a user selects a Drop‑In Pass as a guest and provides an email or phone number, When they complete payment via Apple Pay or Google Pay, Then a verification OTP or magic link is sent to the provided contact within 60 seconds. Given the guest receives the OTP/magic link, When they verify within 5 minutes, Then the pass is bound to the verified contact and current device, and an Auto‑Room Link deep‑opens to the target room within 2 seconds with the preselected micro‑commitment primed for one‑tap check‑in. When the OTP is entered incorrectly 5 times or expires, Then verification is blocked, the pass remains unbound, and a resend option is shown with a 30‑second cooldown. Then the verification UI is accessible (screen‑reader labels present, contrast AA, focus states) and localized to the device locale.
Seamless Guest Re‑Entry and Device Change Handling
Given an active, verified guest pass, When the guest reopens the app or Auto‑Room Link on the same device within the pass validity window, Then they are auto‑authenticated and taken directly to the room without re‑verification. Given the guest attempts access on a different device or browser, When they complete OTP or magic link verification to the same contact, Then the guest profile is restored on the new device, the prior device session is revoked, and only one active session remains. Then streak state, credits, and in‑progress check‑in context are preserved across re‑entry; no duplicate guest profiles are created for the same verified contact.
Receipts and Communication for Guest Profiles
Given payment succeeds, Then a transaction receipt is sent to the verified email or SMS within 2 minutes including purchase details, pass validity window, and a link to manage guest data and receipts. Then only minimal guest data is stored: verified contact, country/region (derived), payment transaction ID/token, consent flags, and pass metadata; no password is collected. When a user opts out of marketing communications, Then only transactional messages are sent and unsubscribe is honored within 24 hours.
Upgrade Guest to Full Account with History Preservation
Given a verified guest completes a session or selects Upgrade, When they create an account using the same verified contact or an SSO that confirms that contact, Then the guest profile is merged into the new account, preserving pass purchase record, streak history, credits, and check‑ins. Given an existing account already matches the verified contact, When the guest selects Upgrade, Then they are prompted to sign in and the guest data is merged into that existing account without creating duplicates. Then the migration completes within 5 seconds server‑side or surfaces a retry option while retaining guest access; no data loss is permitted.
Timeboxed Access and Rolling Window Enforcement for Guests
Given a guest pass bound to a specific session and rolling window, When the current time is within that window, Then the guest can enter the room and check in; outside the window, access is denied with a clear message and CTAs to purchase another pass or subscribe. When a room’s start or end time shifts after purchase, Then the pass validity adjusts per policy and is recorded, the guest is notified, and streak integrity is not penalized. When SafeTap Undo is invoked within its allowed period, Then the check‑in is reverted once per session and streak state updates accordingly.
Guest Data Privacy, Retention, and Deletion Compliance
Given a guest profile, When the region‑specific retention period elapses (e.g., 30 days after last activity), Then personal data is deleted or anonymized automatically, retaining only legally required financial records with pseudonymous references. When a guest requests deletion via the receipt link or settings, Then identity is re‑verified and all personal data is deleted/anonymized within 30 days and a confirmation is sent to the verified contact. When a data export (DSAR) is requested, Then a machine‑readable export of the guest’s data is provided within 30 days; all actions are logged for audit with minimal PII.
Verification Security, Token Expiry, and Abuse Protection
Given OTP or magic link verification, Then tokens are single‑use, expire in 5 minutes, and are rate‑limited to 5 attempts per hour per contact and device; resend is throttled with a 30‑second cooldown. When a magic link is used, Then verification occurs over HTTPS, deep‑opens the target room, binds the current device, and any reuse of the link shows an error with a new‑link option. Then all verification events are logged with timestamp, contact hash, IP country, and outcome for security analysis; PII is minimized and encrypted at rest and in transit.
Streak Integrity Guard & Metrics Isolation
"As a regular subscriber, I want drop‑in activity to be clearly separated from my subscription streak so that my progress remains accurate and fair."
Description

Ensure Drop‑In check‑ins are tagged and isolated from subscription streak logic to avoid unintended streak inflation or decay. Maintain separate counters and eligibility flags, and define conversion rules that allow seamless upgrading to a subscription without penalizing or resetting legitimate streaks. Update leaderboards, room stats, and notifications to display drop‑ins appropriately. Provide analytics segmentation to compare conversion and retention outcomes of drop‑in users versus subscribers.

Acceptance Criteria
Drop-In Check-In Isolation from Subscription Streaks
Given a user without a subscription completes a Drop-In check-in in a room When daily streak calculation runs Then no subscription streak is created And the drop-in check-in is recorded with check_in_type=drop_in and subscription_eligible=false Given a subscriber with an active streak of N completes a Drop-In check-in but no subscription check-in that day When daily streak calculation runs Then their subscription streak decays per standard rules and is not preserved by the drop-in And the drop-in counter increments by 1 Given any report that aggregates streak counts When it includes users with only drop-in check-ins for the period Then those users are excluded from subscription streak totals
Conversion on Same-Day Upgrade Without Double Counting
Given a user completes a Drop-In check-in at time T1 and purchases a subscription at time T2 on the same calendar day (room local time) When conversion logic executes Then the drop-in check-in is converted to a subscription check-in exactly once (converted=true, check_in_type updated to subscription) And no duplicate check-in record is created And drop-in counters for that day decrement accordingly Given a user upgrades on a different calendar day than their last drop-in When conversion logic executes Then no historical drop-ins are converted And the subscription streak begins only after the first subscription check-in Given a user already has a subscription streak and completes a Drop-In in a different room When daily streak calculation runs Then only subscription check-ins contribute to the subscriber’s streak regardless of drop-in activity in other rooms
Leaderboards and Room Stats Reflect Drop-Ins Without Affecting Streak Rankings
Given a room leaderboard ranked by streak length When a user performs only drop-in check-ins during the period Then their rank and displayed streak remain unchanged And a visible Drop-In badge is shown next to their name Given room participation metrics (attendance, reactions) When drop-in users participate Then attendance includes drop-ins And average streak and longest streak metrics exclude drop-in-only activity Given a converted check-in (converted=true) When leaderboards and stats refresh Then the user’s streak count updates based on the converted subscription check-in And the Drop-In badge for that entry is removed
Notifications and Reminders Honor Streak Integrity for Drop-Ins
Given a user completes a Drop-In and no subscription check-in that day When notifications are sent Then no “streak maintained” notification is sent And a “Drop-In completed” notification is sent Given a user receives end-of-day risk notifications When the day contains only drop-in activity Then “streak at risk” or “streak lost” notifications are sent per standard rules and are not suppressed by drop-ins Given a same-day upgrade with conversion When post-purchase notifications are sent Then the user is informed that today’s check-in counts toward their subscription streak And no duplicate check-in prompts are sent
Analytics Segmentation for Drop-Ins vs Subscribers
Given analytics events are emitted for check-ins and purchases When events are inspected Then each event includes properties: user_plan_status in {drop_in_only, subscriber}, check_in_type in {drop_in, subscription}, and converted_from_drop_in in {true,false} Given dashboards for conversion and retention When filtering by user_plan_status Then conversion rate from drop-in-only to subscriber and D1/D7 retention for each segment can be computed without cross-contamination Given cohort exports When querying for subscription streak retention Then users with only drop-in activity are excluded from subscription retention cohorts by default
SafeTap Undo and Reconciliation for Drop-Ins
Given a user completes a Drop-In and then taps Undo within the allowed window When records are updated Then the drop-in check-in is removed, drop-in counters decrement, analytics emit check_in_undo with check_in_type=drop_in And the subscription streak remains unchanged Given a drop-in check-in was converted to subscription on the same day When the user taps Undo within the allowed window Then the converted subscription check-in is removed, conversion flags clear, streak recalculates accordingly And no residual drop-in record persists Given Undo occurs after the allowed window When the user attempts Undo Then the action is blocked with a clear message And no counters or streaks are modified
API/DB Contract Ensures Separate Counters and Eligibility Flags
Given the public API for check-ins When retrieving a check-in Then the payload includes fields: check_in_id, user_id, room_id, check_in_type, subscription_eligible, converted_from_drop_in, occurred_at And these fields are non-null and validated against an enum Given the streak calculation service When reading check-ins Then only records with check_in_type=subscription and subscription_eligible=true are considered for streak continuity Given data validation jobs When they run daily Then any record where check_in_type=drop_in and subscription_eligible=true is flagged and reported as a data integrity violation with zero false positives in seeded test data

Gift Pass

Let supporters gift paid access to friends or teammates via secure links or QR codes. Recipients claim with Scan‑to‑Sign passkeys—no passwords—and auto‑join under a pseudonym with Time Blur defaults. Bulk gifts and team packs make it simple for Room Orchestrators to sponsor seats. Benefits: organic room growth, feel‑good support moments, and privacy‑preserving gifting that keeps identities separate.

Requirements

Gift Pass Purchase & Inventory
"As a supporter, I want to purchase gift passes and see how many are unclaimed or used so that I can easily sponsor access for friends or my team and track my impact."
Description

Enable supporters to buy individual gift passes or team seat packs using existing billing rails. Create a pass inventory ledger that tracks pass IDs, seat counts, purchaser, price, taxes, status (unclaimed, claimed, expired, revoked), and optional expiration windows. Provide a “My Gift Passes” management view for supporters and a “Sponsored Seats” view for Room Orchestrators to monitor allocation and usage. Support refunds/cancellations that automatically revoke unclaimed passes and adjust ledger balances. Allow optional room association tags to pre-assign passes to a room or leave them unscoped for general use. Ensure receipts and invoicing meet regional tax requirements and that purchase flows work on mobile and web.

Acceptance Criteria
Single Gift Pass Purchase on Mobile and Web
Given a logged-in supporter with a valid payment method on a supported device (iOS/Android/web) When they purchase exactly 1 gift pass via existing billing rails without a room tag or expiration Then the payment is authorized and captured successfully, a receipt is generated, and the UI shows a confirmation with a pass link/QR and receipt link And a ledger record is created with: pass_id (UUID), seat_count = 1, purchaser_id, currency, unit_price, tax_amount, total_amount, status = "unclaimed", created_at timestamp, expiration = null And the same flow is available and functional on both mobile apps and web with identical outcomes and data written to the ledger
Team Seat Pack Purchase and Allocation
Given a supporter selects N seats (2–200) for a team pack When checkout completes successfully Then the ledger stores a parent order with seat_count = N and generates N unique pass_ids/claim tokens linked to the order And the receipt/invoice itemizes quantity N, unit_price, tax_amount, discounts (if any), and total_amount And management views show remaining_unclaimed = N − claimed_count in real time
Pass Inventory Ledger Lifecycle
Given a ledger entry in status "unclaimed" When a recipient claims the pass Then status transitions to "claimed", claimed_at is set, and purchaser_id, unit_price, and tax_amount remain immutable When an unclaimed pass reaches its expiration window Then status transitions to "expired" and the claim link/QR becomes invalid When a purchaser revokes an unclaimed pass Then status transitions to "revoked" and the claim link/QR is invalidated immediately And all status changes are audit-logged with actor, timestamp, and reason, and invalid transitions (e.g., claimed → unclaimed) are rejected with an error
My Gift Passes Management View for Supporters
Given the purchaser opens My Gift Passes Then a paginated list of passes is shown with filters by status (unclaimed, claimed, expired, revoked), search by tag/room, and sort by created_at And unclaimed passes support actions: copy link, show QR, revoke, set/remove expiration, add/remove room tag And claimed passes show read-only details with no revoke action And aggregate counts (total, claimed, unclaimed) match the ledger, and the view is responsive on mobile and web
Sponsored Seats View and Room Association Tags for Orchestrators
Given a Room Orchestrator opens Sponsored Seats for a room Then they see: total seats tagged to that room, claimed seats, remaining seats, and per-purchaser allocation counts without recipient PII And they can tag unscoped unclaimed passes they own (or have manage rights for) to the room and untag them, but cannot retag claimed seats And tag changes update immediately in both Sponsored Seats and My Gift Passes and are persisted in the ledger
Refunds and Cancellations Adjust Ledger
Given a supporter requests a refund/cancellation When a refund is approved for unclaimed passes Then those passes are set to status "revoked", claim links are invalidated, ledger refund_amount is recorded, and counts update accordingly And partial refunds on team packs revoke only the specified number of unclaimed seats; claimed seats remain unaffected And payment provider webhooks reconcile refunds idempotently, and receipts/invoices are updated with credit notes or refund annotations
Receipts and Invoicing with Regional Tax Compliance
Given checkout is initiated with billing country/state and optional tax ID When the purchase completes Then taxes are calculated per regional rules (e.g., VAT/GST), with displayed pricing reflecting required tax-included/excluded presentation, and invoice includes legal entity, purchaser details, tax rate/amount, currency, and a unique invoice number And receipts are emailed to the purchaser, downloadable as PDF in-app, and attached to the ledger record And valid tax-exempt or reverse-charge scenarios result in zero-rated tax with recorded tax ID validation status on the invoice
Secure Link & QR Issuance
"As a supporter, I want secure shareable links or QR codes for each gifted seat so that recipients can claim access quickly without exposing my account or payment details."
Description

Generate unique, signed, single-use claim URLs and corresponding QR codes for each pass or seat, compatible with deep links across iOS, Android, and web. Allow creators to set expiration TTLs, usage limits (one claim per token), and optional room scoping at generation time. Provide revocation and regeneration controls that immediately invalidate old tokens. Implement clipboard/share sheet integrations and downloadable/printable QR assets for events. Harden tokens with short-lived signatures, audience binding, and tamper detection while keeping distribution friction low.

Acceptance Criteria
Secure Single-Use Link Issuance & Claim
Given a creator requests a gift pass link with usage limit = 1 and TTL = 24h When the system generates the link Then the URL includes a unique token ID and a signed payload with iat, exp, issuer, and audience = "streakshare:gifting:claim" And the token cannot be derived from or guessed by ≤ 2^128 attempts And an audit log entry is recorded with issuer ID and token ID only (no recipient PII) When a recipient opens the link within TTL Then the server validates signature, audience, not-before, and expiry And prompts Scan‑to‑Sign passkey And on successful assertion creates a pseudonymous account with Time Blur defaults enabled And marks token status = consumed And returns 201 with seat assignment When any second claim attempt occurs after consumption Then the server returns 409 TOKEN_ALREADY_CLAIMED and creates no additional seat
QR Code Generation & Scan-to-Sign Claim
Given a claim URL exists When the issuer requests a QR code Then the system provides PNG and SVG assets at ≥ 1024×1024 px with ≥ 20% quiet zone and high-contrast foreground/background And the QR encodes the exact HTTPS claim URL When the QR is scanned on iOS or Android Then the deep link opens the app if installed or routes to the store and returns to the Claim screen after install with token preserved When the QR is scanned via desktop camera Then the web claim page opens with token preserved When printed at 300 DPI on A5 and scanned from 1 meter under 200–500 lux lighting Then decode success rate is ≥ 99% on reference devices
TTL Expiration Enforcement
Given a creator sets TTL T within [15 minutes, 30 days] When current time > issued_at + T Then claim attempts return 410 TOKEN_EXPIRED and the token remains unconsumed And an audit log records an expiry event When current time ≤ issued_at + T and the token is not consumed or revoked Then claim proceeds normally
Revocation & Regeneration Immediate Invalidation
Given a token is active When the issuer selects Revoke Then all claim attempts made ≥ 5 seconds after revocation return 403 TOKEN_REVOKED on iOS, Android, and Web And no seat or account is created When the issuer selects Regenerate Then a new token ID, signature, URL, and QR are created And the previous token becomes invalid within ≤ 5 seconds And seat allocation quota/entitlement remains unchanged
Cross-Platform Deep Link Routing
Given a valid claim URL When opened on iOS with the app installed Then it routes to the in-app Claim screen within ≤ 2 taps When opened on iOS without the app installed Then it routes to the App Store and returns to the Claim screen after install with token preserved When opened on Android Then it routes to the in-app Claim screen or Play Store fallback with token preserved When opened on desktop Then it routes to the Web Claim page In all cases Then the token is not exposed in OS notification previews or link previews And the complete claim flow time is ≤ 10 seconds on 4G (80th percentile)
Room-Scoped Pass Enforcement
Given a token is created with room scope R When the token is claimed Then the recipient auto-joins room R under a pseudonym and R appears in their home And the issuer cannot view the recipient’s identity When the scoped room is archived or deleted before claim Then the claim returns 409 ROOM_UNAVAILABLE and the token remains unconsumed Given a token without room scope When the token is claimed Then no auto-join occurs
Share Sheet, Clipboard, and Printable Assets
Given a generated claim link When the issuer taps Share on iOS or Android Then the native share sheet opens with a prefilled title and the HTTPS short link (≤ 120 characters) When the issuer taps Copy Link Then the link is placed on the clipboard and a confirmation toast appears within ≤ 1 second Given a web browser that supports navigator.share When Share is invoked Then navigator.share is used; otherwise a copy-to-clipboard fallback is shown When QR assets are downloaded Then PNG and SVG files are provided with filenames including the token ID suffix and contain no embedded PII
Scan‑to‑Sign Passkey Claim
"As a recipient, I want to claim a gifted seat using my device’s passkey so that I can join instantly without creating or remembering a password."
Description

Deliver a passwordless claim flow using WebAuthn passkeys: recipients tap a link or scan a QR, then create/verify a platform passkey to finalize redemption. Support cross‑device “scan to sign” by showing a QR on desktop that opens the mobile app/site to use the device’s passkey. No email/password required. Handle edge cases (token already used, expired, revoked) with clear messaging and safe retry paths. On success, bind the redeemed seat to the new credential, start a session, and redirect to the target room or gift lobby.

Acceptance Criteria
Mobile Gift Link Claim with Platform Passkey
Given a recipient opens a valid, unredeemed gift link on a passkey-capable mobile device When the claim page loads Then the token is prevalidated server-side and a WebAuthn platform passkey prompt is presented with no email/password fields visible Given the recipient has no existing StreakShare passkey on the device When they approve the platform authenticator Then a new passkey credential is created, the gift token is redeemed, the seat is bound to the credential ID, a session is started, and the user is redirected to the target room or gift lobby Given the recipient already has a StreakShare passkey on the device When they approve the platform authenticator Then the existing credential is used to redeem the token, the seat is bound to that credential ID, a session is started, and the user is redirected to the target room or gift lobby
Cross‑Device Scan‑to‑Sign from Desktop
Given a recipient opens a valid, unredeemed gift link on a desktop without a usable platform authenticator When they choose "Scan to sign" Then a QR code is displayed that encodes a deep link to the mobile claim endpoint associated with the same token Given the QR code is scanned on a passkey-capable mobile device When the deep link opens Then the mobile claim page initiates a WebAuthn platform passkey prompt and proceeds to redeem the token upon successful authentication Given the token is successfully redeemed on mobile When the desktop page detects redemption via server-push or polling Then it updates to a success state and offers a Continue action without requiring login
Error States — Used, Expired, or Revoked Token
Given a gift token that is already redeemed When the claim page is accessed via link or QR Then the page displays an "Already claimed" message with a safe next action (e.g., Return to lobby and Use a different gift) and does not start WebAuthn Given a gift token that is expired When accessed Then the page displays an "Expired gift" message with a safe next action (e.g., Contact sender and Request new gift) and does not start WebAuthn Given a gift token that is revoked When accessed Then the page displays a "Gift revoked" message with a safe next action and does not start WebAuthn Given any of these error states When the recipient retries with a different valid token Then the claim flow proceeds normally
Token Single‑Use and Concurrency Safety
Given two clients attempt to redeem the same valid token concurrently When one redemption succeeds Then subsequent attempts receive an "Already claimed" response and no additional seats are bound Given a token redemption succeeds When the server persists binding of seat to credential ID and session creation Then the operation is atomic and idempotent so that retried requests do not create duplicate bindings or sessions
Privacy‑Preserving Claim (No Identity Collection)
Given the claim flow When presented to the recipient Then no email, password, phone number, or name fields are shown or required at any step Given redemption succeeds When the new account context is created Then a pseudonymous display name is auto-assigned and Time Blur defaults are enabled Given the seat is bound and session started When the recipient views their account or room entry state Then no linkage to the sender’s identity is visible to the recipient
Unsupported Device/Browser Handling
Given the recipient’s device or browser does not support WebAuthn platform passkeys When they open the claim link Then the claim page detects the limitation and prominently offers Scan to sign with QR, plus guidance to open on a supported device, and does not offer email/password fallback Given the recipient proceeds with Scan to sign When a supported device is used Then the flow completes as per the cross-device scenario Given the recipient cannot use any supported device When they choose to exit Then a non-blocking help link is provided to contact support without collecting credentials
Pseudonymous Auto‑Join with Time Blur Defaults
"As a privacy‑conscious recipient, I want to auto‑join under a pseudonym with protective defaults so that I can participate without revealing my identity to the sponsor or the room by default."
Description

Automatically provision a pseudonymous profile at claim time with a randomized handle and avatar that are not linkable to the purchaser. Apply privacy‑first defaults, including Time Blur and minimal public metadata. Auto‑join the recipient to the designated room (or a gift lobby if unspecified) and present optional, privacy‑preserving onboarding to customize the handle later. Ensure strict data boundaries: the purchaser sees only aggregate redemption metrics and non‑identifying status, never the recipient’s identity or activity details.

Acceptance Criteria
Scan-to-Sign Claim Creates Pseudonymous Profile
Given a valid, unredeemed gift link or QR code And a device with WebAuthn/Passkey support When the recipient claims the gift and completes passkey registration Then the system provisions a new profile with a randomized, non-identifying handle (8–16 chars, uniqueness-checked) and a randomized avatar from the pseudonymous set And no password fields are presented at any step And no email, phone, or real name is collected or stored during claim And internal profile IDs are not derived from or linkable to the purchaser’s identifiers And the gift token is marked redeemed and cannot be reused or replayed
Privacy Defaults: Time Blur and Minimal Metadata
Given a pseudonymous profile created via gift claim When profile defaults are applied Then Time Blur is enabled by default so non-friends see only coarse activity buckets (e.g., date-level, not exact timestamps) And public metadata is limited to handle and avatar only by default And sharing of streak length, exact activity times, location, device info, and contact discovery is disabled by default And all public/profile API responses exclude personally identifying fields (email, phone, exact timestamps) And disabling Time Blur requires an explicit, informed opt-in by the recipient
Auto-Join Target Room or Gift Lobby
Given the gift is associated with a designated room that has available sponsored seats When the recipient completes gift claim Then the recipient is auto-joined to that room using the pseudonymous profile And the room roster displays only handle and avatar for the recipient And if the designated room is unspecified or out of seats, the recipient is placed into a gift lobby associated with the gift And purchaser and non-admin room members cannot view the recipient’s identity or exact activity timestamps
Purchaser Visibility: Aggregates Only, No Identity or Activity
Given the purchaser views their gift dashboard after redemptions have occurred When inspecting redemption status and usage analytics Then only aggregate metrics are visible (e.g., redeemed count, active seats, 7-day activity rate) And no recipient identifiers (handle, avatar, email, user ID) or detailed activity (events, timestamps, room roster presence) are visible And purchaser-scoped API endpoints return only aggregates with no per-user rows And attempts to access a recipient profile or activity via purchaser context return 403 or a redacted view
Optional Privacy-Preserving Onboarding
Given the recipient is auto-joined after claim When onboarding is presented Then the recipient may optionally customize handle and avatar via a picker that does not suggest from contacts, email, or social graphs And handle inputs are validated to block emails, phone numbers, and full names And handle changes are rate-limited to a maximum of 1 per 24 hours And skipping onboarding leaves Time Blur and minimal metadata defaults unchanged And any customization does not notify or expose identity to the purchaser
Existing Account Claim Preserves Pseudonymity
Given the recipient is already signed in to an existing StreakShare account When claiming a gift via link or QR code with passkey authentication Then the recipient can choose to join with the existing pseudonymous profile (keeping Time Blur on) or create a new pseudonymous profile And in either path, the purchaser cannot view or infer the recipient’s identity or link the claim to any existing account And only one redemption is permitted per gift token, with replay prevented
Team Packs & Seat Management for Orchestrators
"As a Room Orchestrator, I want to buy and manage a pack of sponsored seats so that I can grow my room efficiently and reallocate unused seats when needed."
Description

Provide Room Orchestrators with bulk gifting tools: purchase seat packs, pre‑assign packs to specific rooms, generate batch claim links/QRs, and track utilization by seat status (pending, claimed, reclaimed, expired). Include seat reclamation rules for inactivity after a configurable grace period, transfer workflows, CSV import/export for distribution, and role‑based permissions for admins vs. contributors. Surface clear dashboards and notifications for pending claims and expiring links.

Acceptance Criteria
Purchase Team Pack and Pre‑Assign to Room
Given an Orchestrator with Admin role and a valid payment method When they purchase a pack selecting a quantity between 5 and 500 and optionally pre‑assign a Room Then the transaction is authorized, a Pack record is created with totalSeats=quantity and availableSeats=quantity, and if a Room is selected the pack is bound to roomId and a confirmation receipt is displayed And the pack appears in the Packs list within 5 seconds with metadata: packId, roomId (if any), purchaseDate, expiryDate (if configured), counts pending=0, claimed=0, reclaimed=0, expired=0 And a permissions check blocks purchase attempts by Contributors with a 403 error and disabled UI controls
Generate Batch Claim Links and QR Codes
Given a pack with availableSeats=N When the Orchestrator generates M claim artifacts where 1<=M<=N and sets an expiration date/time (UTC) and optional note Then the system produces exactly M unique single‑use claim URLs and QR codes, marks each seat status=pending, and reduces availableSeats by M And a CSV export is immediately available with columns: seatId, packId, roomId, claimUrl, qrImage, status, expiresAt And attempting to use a claim URL/QR after it is claimed or expired returns an error page and does not allocate a seat And the claim page requires passkey (WebAuthn) authentication; upon success the seat transitions to claimed and the user is added to the pre‑assigned Room; no password fields are shown And the claimant joins under a pseudonym with Time Blur defaults enabled
Seat Status Dashboard and Notifications
Given existing packs and seats across rooms When the Orchestrator opens the Seats dashboard Then the dashboard shows totals and per‑status counts (pending, claimed, reclaimed, expired, available) that equal the sum across packs, with filters for roomId, packId, status, and date range And table/list pagination supports at least 10,000 seats with server‑side filtering; initial load <= 2 seconds for up to 1,000 seats and <= 5 seconds for 10,000 seats And metrics auto‑refresh at least every 30 seconds or via push within 3 seconds of a status change And the system sends notifications: daily digest for pending > 0 older than 3 days, and alerts 72h and 24h before link expiration; notifications are suppressible per pack And clicking a notification deep‑links to the filtered dashboard view
Auto Reclamation After Inactivity Grace Period
Given a pack with reclamationRule enabled and gracePeriodDays=14 When a claimed seat shows no room activity for 14 consecutive days Then the system sends a warning at T‑48h and T‑4h, and on gracePeriod end transitions seat status to reclaimed, removes room access, and increments availableSeats by 1 if the pack is active And the system writes an audit log entry with seatId, user pseudonym, lastActiveAt, reclaimedAt, ruleId, actor=system And reclaimed seats cannot be reclaimed again without a new claim artifact; any prior claim URL is invalidated And Orchestrators can override reclaim for a specific seat before the deadline; override prevents auto‑reclaim and is reflected on the dashboard
Seat Transfer Between Recipients
Given a claimed seat in a room When an Admin initiates a transfer and confirms the action Then the system generates a new single‑use claim URL/QR for the seat, immediately revokes the prior user’s room access, sets seat status=pending, and records an audit entry And on successful claim by the new recipient, seat status becomes claimed and availableSeats remains unchanged And if the new claim is not completed before expiresAt, seat status becomes expired and availableSeats increases by 1; all events are logged And Contributors attempting transfer receive a 403 and see disabled transfer controls
CSV Import/Export for Seat Distribution
Given a pack with availableSeats >= 1 When the Admin exports seats Then a CSV downloads with headers: seatId, packId, roomId, status, expiresAt, claimUrl (only for pending), lastActiveAt, createdAt; no claim tokens are exported for claimed/reclaimed/expired seats And when the Admin imports a CSV using the provided template with up to 5,000 rows (columns per row: recipientContact optional, expiresAt, note) Then the system validates format, rejects malformed rows with line‑level errors, creates pending seats up to availableSeats, reduces availableSeats accordingly, and reports a summary {created, skipped, errors} And imports are idempotent using a client‑provided importId; re‑submitting the same importId does not create duplicates And the operation time for 5,000 rows is <= 60 seconds with progress feedback
Role‑Based Permissions Enforcement
Given project roles Admin and Contributor When permissions are evaluated across seat management actions Then Admins can purchase packs, configure reclamation rules, generate batch links/QRs, transfer seats, import/export CSV, and view costs; Contributors can view the dashboard and send existing pending claim links but cannot purchase, change rules, transfer, or import/export And restricted actions attempted by Contributors return HTTP 403 and emit a UI error; corresponding controls are hidden or disabled based on role And all privileged actions create audit log entries with actorId, action, entityId, timestamp, and outcome
Anti‑Abuse & Fraud Controls
"As a platform owner, I want safeguards against abuse and multi‑use claims so that gifting remains secure and trustworthy without adding friction for legitimate users."
Description

Enforce single redemption per token and bind the redeemed seat to the first verified passkey credential. Add velocity limits for token creation and claim attempts, device/IP anomaly checks, optional CAPTCHA after thresholds, and short token TTLs. Provide monitoring dashboards, alerting for suspicious activity, and immutable audit logs. Preserve privacy by relying on pseudonymous device signals and avoiding unnecessary PII collection.

Acceptance Criteria
Single-Use Token Redemption & Passkey Binding
Given an unredeemed gift token and a recipient without an existing seat, When the recipient completes Scan-to-Sign passkey verification, Then the token is marked redeemed and the seat is bound to the first verified passkey credential ID. Given a redeemed gift token, When any subsequent redemption attempt occurs, Then the attempt is rejected with HTTP 409 TokenRedeemed and no recipient identifiers are revealed. Given a gift token where passkey verification did not complete, When the recipient abandons before verification, Then the token remains redeemable and no seat is provisioned. Given a bound seat, When the recipient adds additional passkeys, Then the seat remains bound to the recipient’s account and the original token cannot be reused. Given a support/admin revoke action, When the seat is revoked, Then the token does not become redeemable and a new token must be issued.
Token Creation Velocity Limits for Orchestrators
Given an Orchestrator account, When they request token creation, Then per-account velocity limits enforce <= 20 tokens per 15 minutes and <= 200 per 24 hours by default. Given a request exceeding a velocity limit, When the threshold is crossed, Then the API responds 429 TooManyRequests with Retry-After set and no tokens are created. Given a purchased pack size N, When tokens are created, Then the total active tokens <= N and cannot exceed the purchased quantity. Given system-wide load spikes, When global thresholds are crossed, Then token creation is throttled proportionally and logged.
Claim Attempt Rate Limiting & CAPTCHA Challenge
Given a device or IP, When failed claim attempts exceed 5 per device per 10 minutes or 20 per IP per 10 minutes, Then subsequent claim attempts require a CAPTCHA challenge. Given a token, When failed claim attempts against that token reach 3 in 10 minutes, Then the token is temporarily protected by CAPTCHA for 30 minutes. Given a presented CAPTCHA, When solved correctly, Then the claim flow proceeds; When failed 3 times, Then the device/IP is blocked for 30 minutes with HTTP 429. Given a successful claim, When the seat is provisioned, Then all related rate-limit counters for that token/device/IP are reset.
Device/IP Anomaly Detection on Redemption
Given a claim attempt, When the device fingerprint or IP reputation indicates high risk (e.g., Tor/VPN detected, ASN change within 5 minutes, impossible geovelocity > 1000 km in 10 minutes), Then a step-up challenge is required (CAPTCHA or additional WebAuthn assertion). Given a step-up challenge, When the user fails or declines, Then the claim is denied with HTTP 423 Locked and the event is logged with a risk score. Given repeated risky attempts from the same pseudonymous device ID (>= 3 within 30 minutes), When another claim is initiated, Then the device is temporarily blocked for 60 minutes.
Token TTL Enforcement
Given a newly created gift token, When the TTL elapses (default 7 days, configurable 1–14 days), Then the token expires and cannot be redeemed. Given a token first viewed, When view occurs, Then a secondary TTL of 24 hours starts; When either TTL expires, Then redemption returns HTTP 410 Expired. Given an expired token, When the Orchestrator attempts to extend it, Then extension is not permitted and a new token must be issued.
Fraud Monitoring Dashboard & Alerting
Given the monitoring dashboard, When a product analyst selects a room and date range, Then the dashboard displays counts and rates for token creation, claims, redemptions, failures, rate-limits, CAPTCHA challenges/solves, anomaly triggers, and alerts, updated within 5 minutes. Given configurable alert thresholds, When failures or anomalies exceed thresholds (e.g., >= 100 failed claims from one ASN in 10 minutes or 3 sigma above 7-day baseline), Then alerts are sent via email and webhook within 2 minutes and are deduplicated for 15 minutes. Given an alert, When a user clicks through, Then the dashboard deep-link opens with pre-applied filters showing the contributing events.
Immutable Audit Log with Pseudonymous Signals
Given any anti-abuse relevant event (token create/view/claim success/fail, rate-limit, CAPTCHA, anomaly, expiry, seat bind/revoke), When the event occurs, Then an append-only log entry is written with UTC timestamp, event type, and pseudonymous identifiers (token ID hash, device ID hash, IP /24 hash+salt, ASN, credential ID hash). Given audit log integrity checks, When a daily verification runs, Then the Merkle/hash chain proves tamper-evidence and mismatches raise an alert. Given access requests, When a user with AuditReader role queries logs, Then results are filterable by time range, room, event type, and are exportable as encrypted CSV/JSON; PII fields are absent by design.
Redemption Notifications & Growth Analytics
"As a supporter, I want notifications and impact metrics when my gifts are claimed so that I feel confident my support is helping rooms grow."
Description

Emit events when passes are created, claimed, expired, or revoked to drive in‑app and email notifications (opt‑in) for supporters and Orchestrators. Provide analytics on claim funnel conversion, time‑to‑claim, room growth attribution from gifts, and retention of gifted users. Offer lightweight exports and privacy‑preserving aggregation so sponsors see impact without recipient identities.

Acceptance Criteria
Event Emission for Gift Pass Lifecycle
- On pass creation, emit event "pass.created" to the event bus within 1s of persistence with fields: event_id (UUID), event_type, pass_id, sponsor_id, sponsor_role (supporter|orchestrator), room_id, pack_id (nullable), occurred_at (ISO8601 UTC). - On successful claim, emit event "pass.claimed" within 1s with fields: event_id, event_type, pass_id, claimant_user_id (pseudonymous), sponsor_id, sponsor_role, room_id, pack_id (nullable), occurred_at. - On expiration without claim, emit event "pass.expired" within 1s with fields: event_id, event_type, pass_id, sponsor_id, room_id, occurred_at. - On sponsor-initiated revoke, emit event "pass.revoked" within 1s with fields: event_id, event_type, pass_id, sponsor_id, room_id, occurred_at, reason_code. - All events are delivered at-least-once with deduplication via event_id idempotency; retries backoff up to 3 attempts; no duplicate downstream notifications for the same event_id. - Events are partitioned by room_id and ordered per pass_id; clock skew does not invert per-pass ordering.
Notification Triggers and Preferences
- For each lifecycle event (created, claimed, expired, revoked), send in-app notifications to the sponsor_of_pass and room_orchestrators within 2s of event receipt. - Send email notifications only if the recipient has opted-in and has a verified email; emails dispatch within 5 minutes of event receipt. - Respect per-user notification preferences by event type and channel; no notification is sent on channels the user disabled. - Do not include recipient identity in any notification content; for claims, display pseudonym or anonymized label only. - Rate limit: max 1 notification per recipient per pass per event type; duplicates (same event_id) are not re-sent. - Provide actionable links to view aggregated gift impact; links require authenticated access. - Unsubscribe links are present on emails and immediately honored for future sends.
Claim Funnel Conversion and Time-to-Claim Analytics
- Dashboard reports funnel counts and rates for passes: Created → Claimed, with Expired and Revoked shown as distinct drop-offs. - Conversion rate = Claimed / (Created − Revoked_before_claim); displayed to one decimal place. - Time-to-claim metrics computed as median and P90 of (claimed.occurred_at − created.occurred_at) for selected range; units auto-scale (minutes/hours/days). - Filters: by room_id, sponsor_id, sponsor_role, pack_id (nullable), and date range (created_at) with UTC normalization. - Data freshness: metrics reflect events up to the last 15 minutes; timestamp of last update shown. - Counts are de-duplicated per pass_id; partial data during processing never overstates conversion. - Export button provides CSV of aggregated funnel and time-to-claim metrics for the current filter (see privacy/export rules).
Gift-Attributed Room Growth Reporting
- On pass.claimed, attribute a "gift_join" to the room_id when the claimant becomes a member and was not a member in the prior 30 days. - Do not attribute growth if the claimant was already an active member of the room within the prior 30 days or if the pass was revoked after claim. - Growth metrics report: net_new_members_from_gifts, total_claims, attribution_rate = net_new_members_from_gifts / total_claims. - Breakdowns available by room_id, sponsor_id, sponsor_role, pack_id, and week (UTC, ISO week). - Double-counting is prevented across multiple claims by the same user in the same room within 30 days. - Visualization includes time series and totals; all values respect privacy thresholds before display. - Export provides aggregated growth metrics for the selected breakdowns (see privacy/export rules).
Retention Metrics for Gifted Users
- Cohorts are formed by claim_week (UTC) of pass.claimed; users are included once per room. - Retention is defined as the percentage of users in the cohort who perform at least one habit check-in in the room on D1, D7, and D30 relative to their claim day (calendar-day buckets, UTC). - Metrics displayed: D1, D7, D30 retention percentages, cohort size (n), and active counts; values update daily at 02:00 UTC. - Users whose claims were later revoked remain in the cohort but are excluded from retained counts post-revoke date. - Filters: room_id, sponsor_id, sponsor_role, pack_id, date range of claim_week. - Privacy: cohorts with n < 5 are suppressed; no user-level drill-down is available from retention views. - Export provides cohort-level aggregated retention metrics only (see privacy/export rules).
Privacy-Preserving Aggregations and Lightweight Exports
- No analytics view or export includes recipient real names, emails, phone numbers, or stable identifiers; only aggregated metrics are exposed. - Apply privacy thresholds: suppress any aggregated cell where contributing unique claimants < 5; display “insufficient data” instead. - Apply time blur: all analytics timestamps are bucketed to day or week; no per-event timestamps are shown outside exports of aggregate bins. - CSV exports are limited to aggregated metrics for the current filters; include columns: dimension(s), metric, value, period_start, period_end, generated_at (UTC), version. - Export generation completes within 2 minutes for datasets ≤ 10,000 rows; files expire and are deleted after 24 hours. - Access control: only the sponsor_of_pass and room_orchestrators with analytics permission can view or export; access is audited with actor_id and generated_at. - Differential privacy or rounding to nearest 1 for counts is applied where necessary to prevent re-identification at thresholds.

Supporter Afterglow

An optional, supporter‑only cooldown right after a session: a 3–10 minute micro‑room with one focused prompt from the host and lightweight reactions. Late joiners get a two‑hour asynchronous window to check in within their rolling window. Badges shine subtly; identities stay pseudonymous. Benefits: deeper connection without chat noise, meaningful upsell value, and a gentle nudge that improves next‑day adherence.

Requirements

Afterglow Access Control & Eligibility
"As a supporter, I want access to an exclusive Afterglow immediately after my session so that I can reflect with peers without general chat noise."
Description

Server-enforced gating ensures only active supporters can see and enter the Afterglow micro-room. The room is created only when the host has enabled Afterglow for the session, with eligibility checked against the membership subsystem at join time and refreshed on token renewal. Non-supporters see a non-intrusive teaser surface but cannot view content or participant identities. The join surface appears immediately at session end for eligible users and degrades gracefully when network or entitlement checks fail. Integration points: membership/entitlements, session service, client navigation, and push targets. Performance target: render join surface within 1 second of session end under p95. Security: prevent URL sharing leaks by validating access on each socket connection and API call.

Acceptance Criteria
Eligible supporter sees Afterglow join surface at session end (p95 ≤ 1s)
Given an authenticated user with an active supporter entitlement for the host And the current session has Afterglow enabled When the client receives the session_ended signal Then the Afterglow join surface renders within 1,000 ms at the 95th percentile as measured by client telemetry And the Join action is enabled and navigates to the Afterglow room on tap
Non‑supporter teaser shows without content or identities
Given a user without an active supporter entitlement When the session ends or the user opens an Afterglow deep link Then the app displays a single non‑intrusive teaser with an Upgrade CTA and a one‑tap dismiss And no Afterglow content, participant list, avatars, names, badges, or counts are shown And any Afterglow content/API calls return 403 Forbidden and emit no participant or room metadata
Room only created when host enabled Afterglow
Given a session where the host has Afterglow disabled When the session ends Then no Afterglow room is created and no join surface is emitted to clients Given a session where the host has Afterglow enabled When the session ends Then the server creates the Afterglow room and emits availability to eligible supporters within 1,000 ms
Per‑request entitlement validation blocks shared URLs and sockets
Given an ineligible user attempts to access via a shared Afterglow URL or socket token When the client calls any Afterglow API or opens a socket Then the server validates entitlement on that request, returns 403 Forbidden, sends no room/participant data, and closes the socket And an audit log entry is recorded with reason=entitlement_denied and no PII in the response Given an eligible supporter performs the same actions When the request is processed Then the request is authorized and room data streams successfully
Entitlement re‑check on token renewal revokes or grants access
Given a user connected to an Afterglow room And their auth token is renewed during the session When the server revalidates membership entitlement on renewal Then if entitlement is no longer active, the server sends entitlement_revoked, stops data, and the client exits to the teaser within 3 seconds And if entitlement has become active, subsequent join/navigation attempts succeed without app restart
Graceful degradation on network or entitlement service failure
Given the entitlement service times out or returns 5xx at join When the client attempts to validate access Then the client shows a non‑blocking error with Retry and does not display any Afterglow content or identities And telemetry logs error_code=ENTITLEMENT_UNAVAILABLE and the UI remains usable Given the device is offline When the user taps Join Then an offline message is shown with Retry and no automatic retries exceed one additional attempt
Late joiner access within two‑hour asynchronous window (supporters only)
Given a supporter who did not attend live When they attempt to join within two hours after session end (server time) Then access is granted and the room loads or check‑in is available as designed Given the same user attempts at or after T+2h When they try to join Then access is denied with reason=window_closed and the teaser is shown And non‑supporters at any time receive only the teaser and no content
Auto Afterglow Session Orchestration
"As a host, I want an automatic 3–10 minute Afterglow to start when my session ends so that supporters can reflect with a clear, focused cooldown window."
Description

Automatically spin up a time-boxed Afterglow micro-room (3–10 minutes) when a live session ends, using host-configured defaults. Provide a visible countdown timer, hard stop on expiry, and a single optional 1-minute extend action for the host within the 10-minute maximum. Handle presence, join/leave, and capacity, with real-time state synced over the existing session transport. On timeout, close sockets, finalize participation state, and persist minimal metadata (duration, participant count, prompt ID). Deliver instant in-app and push notifications to eligible supporters when the room opens. Resilience: idempotent room creation on duplicate end-of-session events; safe fallback if the prior session overruns or multiple rooms would collide.

Acceptance Criteria
Auto-Creation on Session End (Idempotent)
Given a live session with ID S ends and the host has Afterglow enabled with defaults set (duration D 3–10 minutes, prompt ID P, capacity C) When the platform processes the end-of-session event for S Then exactly one Afterglow room R is created with duration D, prompt P, and capacity C, and R is associated to S And an idempotency key equal to S ensures duplicate end events within 10 minutes return R and do not create a new room And if an Afterglow room already exists for S, no new room is created and the existing room ID is returned And if host defaults are missing/invalid, system falls back to app defaults (duration 5 minutes, capacity 50, prompt null)
Time-Boxing, Countdown, and Single Extend
Given an Afterglow room R is open with remaining time T When a client joins R Then a visible countdown shows T with 1-second resolution and stays synchronized within ±1 second across clients When the host taps Extend Then the remaining time increases by 60 seconds only if total duration ≤ 10 minutes and no prior extend occurred And the Extend action becomes disabled after one successful extend When the timer reaches 0 Then R hard-stops, blocks new joins, and all clients are transitioned to the ended state within 2 seconds
Presence, Join/Leave, Capacity, and Transport Sync
Given supporters attempt to join R over the existing session transport channel When a supporter joins Then their pseudonymous identity appears in presence and participant count updates for all connected clients within 1 second When a participant disconnects or leaves Then presence and counts update within 3 seconds When capacity C is reached Then additional join attempts receive a Room Full response within 1 second and are not connected And non-supporters are denied join with an upgrade CTA And all presence and state events are emitted on the existing session transport namespace; no new transport handshake is required
Notifications to Eligible Supporters on Room Open
Given R is created and opened When notifications are triggered Then in-app notifications are delivered to online eligible supporters within 3 seconds of room open And push notifications are dispatched to offline eligible supporters within 15 seconds with a collapse key preventing duplicates per room And users who are not eligible supporters or have notification opt-outs receive no notification And duplicate room-open events do not cause duplicate notifications
Timeout Finalization and Metadata Persistence
Given R is closing due to timer expiry or host manual close When closure is initiated Then the server closes all participant sockets within 2 seconds and rejects new joins And participation state is finalized for users who were connected at any point during R And minimal metadata is persisted: room_id, session_id S, prompt_id P, configured_duration, actual_duration_seconds, unique_participant_count, started_at, ended_at And the persisted record is queryable via API within 5 seconds of closure
Overrun and Collision Safe Fallback
Given a live session overrun or back-to-back sessions produce multiple end events for the same host When Afterglow creation is evaluated Then at most one active Afterglow exists per host at any time And if an Afterglow is already active for S or the host, subsequent creations are suppressed and return the active room ID without side effects And all suppressed attempts are logged with a reason code (IDEMPOTENT_DUPLICATE or COLLISION_SUPPRESSED)
Single-Prompt Composer & Library
"As a host, I want to pick one focused prompt for Afterglow so that participants have a clear anchor for reflection without chat noise."
Description

Enable hosts to select or pre-schedule a single focused prompt for the Afterglow from a curated library or compose a custom one-line prompt with character limits and profanity filtering. Provide defaults per session type if the host takes no action. Show the prompt prominently in the micro-room header and in notifications. Support preflight validation, lightweight moderation hooks, and localization for templates. Allow editing until the Afterglow opens, with changes versioned and broadcast atomically. Store prompt selection minimally for analytics without retaining conversation data.

Acceptance Criteria
Library Prompt Selection & Scheduling
Given I am a host creating or editing a session When I open the Afterglow prompt library, select exactly one template, and schedule the session Then the selected prompt is associated to the session as the single active prompt and is scheduled Given the session reaches the Afterglow start time When activation occurs Then the scheduled prompt becomes active and is available to clients within 5 seconds Given a prompt is already selected for the session When I attempt to select an additional prompt Then the system prevents multiple selections and shows a single-prompt constraint message
Custom Prompt Composition With Limits & Profanity Filter
Given I compose a custom one-line prompt When the text exceeds the configured character limit (e.g., 120 characters) Then save is blocked and the UI shows the remaining allowed characters Given I compose a custom one-line prompt When the text contains profanity per the locale-specific list Then save is blocked, a moderation event is recorded, and a non-sensitive violation message is shown Given I compose a custom one-line prompt When the text is non-empty, single-line, within the character limit, and passes profanity checks Then the prompt saves successfully and can be scheduled or set as current
Default Prompt Fallback by Session Type
Given no prompt has been selected or composed by the host by the Afterglow start When the Afterglow opens Then a session-type-specific default prompt is automatically assigned and displayed Given a default prompt is required When the session locale is applied Then the default template is chosen in that locale or falls back to base language and then English if unavailable Given a default prompt is auto-assigned When analytics are recorded Then the selection_source is recorded as default without storing the prompt text
Prompt Display in Header and Notifications
Given the Afterglow is active When a participant opens the micro-room Then the selected prompt is visible at the top header area above reactions on all supported viewports Given notifications are enabled for a participant When the Afterglow begins Then the push and in-app notification payloads include the prompt text (localized) and the session name Given a participant joins within the 2-hour late check-in window When the system sends the late-join notification Then the prompt is included and the deep link opens the micro-room with the same prompt visible in the header
Edit Until Open, Versioning, and Atomic Broadcast
Given a prompt is scheduled for a session When the host edits the prompt before the Afterglow opens Then a new version is created, the prior version is retained, and the scheduled time remains unchanged Given an edit is attempted at or after the Afterglow open time When the host saves Then the edit is rejected with an edit-locked message and no participants see mixed content Given a prompt version is published When participants have the micro-room open Then all clients display the same version after a single refresh within 5 seconds of publish
Preflight Validation and Moderation Hooks
Given I attempt to save or schedule a prompt When preflight runs Then it validates non-empty text, single-line, length limit, locale availability (for templates), and absence of profanity Given moderation hooks are enabled When a prompt triggers a policy violation Then saving is blocked, a moderation event with prompt hash, locale, and session_id is emitted, and the UI displays a generic violation message Given a prompt passes preflight and moderation When I save Then no additional approval is required and the prompt is available for scheduling or activation
Minimal Analytics Storage (No Conversation Data)
Given any prompt is selected, edited, or activated When analytics data is written Then only prompt_id or template_id, selection_source (custom|library|default), locale, version, timestamps, and session_id are stored; prompt free text is not stored Given a custom prompt is used When data retention policies execute Then the prompt text is discarded after validation and is not retrievable via analytics or export Given analytics exports or dashboards are generated When fields are inspected Then no conversation messages or prompt free text are present
Lightweight Reactions-Only Feedback
"As a participant, I want to respond with quick reactions instead of chat so that I can engage meaningfully without distraction or effort."
Description

Offer a constrained set of one-tap reactions (e.g., check, clap, heart, spark) in lieu of free-text chat to keep engagement low-friction and noise-free. Show per-reaction counts and a subtle personal acknowledgment without revealing detailed participant lists. Enforce rate limits and spam protection on the client and server, with optimistic UI that reconciles on acknowledgement. Provide accessible alternatives (labels, haptics off, reduced motion) and ensure reactions consume minimal bandwidth. Persist aggregate counts to the session record; no user-level reaction history is exposed. Integrate with streak logic so that a check-in reaction can complete the Afterglow action when applicable.

Acceptance Criteria
Constrained Reaction Set in Supporter Afterglow
Given a supporter is in an active Afterglow micro-room, when they open the reaction UI, then only four one-tap reactions are available: check, clap, heart, spark. Given the Afterglow micro-room is active, when the user attempts to open or use any text chat input, then no text input control is rendered and the keyboard does not appear. Given a reaction button is tapped, when the tap is registered, then the reaction is submitted with a single tap and no additional modal, text field, or confirmation dialog is shown.
Aggregate Counts Display with Personal Acknowledgment and Privacy
Given participants are sending reactions, when viewing the reaction bar, then per-reaction aggregate counts are displayed as non-negative integers and update within 2 seconds (p95) of server receipt. Given I send a reaction, when the client acknowledges, then I see a subtle personal acknowledgment (e.g., brief pulse or “Sent”) without any list of other participants or user identifiers being shown. Given the counts API/stream is inspected, when payloads are received, then they contain only aggregate counts per reaction type and no user identifiers or per-user reaction data.
Client and Server Rate Limiting and Spam Protection
Given a user is reacting, when they send reactions, then the client enforces a maximum of 6 reactions per 10 seconds per Afterglow and disables affected buttons during the cooldown with a non-blocking notice. Given requests exceed server policy, when more than 10 reactions per minute per user per Afterglow are received (burst up to 5), then the server returns HTTP 429 with a Retry-After value and the client backs off accordingly. Given three or more 429 responses occur within two minutes, when the user continues to tap, then reaction inputs are muted for 60 seconds and a subtle “Rate limited” toast is shown. Given duplicate taps or network retries occur, when an idempotency key matches a recent submission (≤5 seconds), then the server deduplicates and the aggregate count increments at most once.
Optimistic UI with Acknowledgment Reconciliation
Given a reaction is tapped, when the request is dispatched, then the UI applies an optimistic selected state and increments the local count within 100 ms. Given the server responds 2xx, when the acknowledgment arrives, then the optimistic state is confirmed and no visual jump exceeds one increment. Given the server rejects (4xx/5xx) or no response is received within 5 seconds, when the error condition is detected, then the optimistic increment is reverted within 200 ms and a subtle error toast appears without blocking further interaction. Given server aggregate updates arrive out of order, when a newer authoritative count is received, then the client reconciles to the server value within 500 ms without producing negative counts or double increments.
Accessibility for Reactions
Given a screen reader is enabled, when focus moves to a reaction button, then it announces an accessible name and hint (e.g., “Clap, button”) and the current aggregate count. Given the user has disabled haptics in settings, when a reaction is sent, then no haptic feedback is triggered. Given the user has enabled Reduced Motion, when reactions are sent or received, then animations are replaced with non-motion alternatives (e.g., fade) and no confetti/parallax effects are shown. Given WCAG AA targets, when rendering reaction UI, then text/icon contrast meets ≥4.5:1 for normal text and ≥3:1 for large icons, and visible focus indicators are present.
Minimal Bandwidth Usage for Reaction Events
Given a reaction event is transmitted, when inspecting the wire payload, then its uncompressed size is ≤350 bytes and contains no images, fonts, or verbose metadata. Given a 10-minute Afterglow with an average of 20 reactions per user, when measuring at p95, then total reaction-related data transferred per user is ≤50 KB. Given aggregate count updates are streamed, when the room is active, then updates are batched to at most one per second over a persistent connection; if on HTTP fallback, polling occurs no more than once every 3 seconds.
Check-in Reaction Completes Afterglow and Updates Streak
Given a supporter is eligible to complete the Afterglow, when they send a “check” reaction during the live Afterglow window, then the Afterglow action is marked complete and their streak updates per streak rules within 5 seconds. Given a late joiner within two hours after session end and within their rolling window, when they send a “check” reaction via the asynchronous flow, then the Afterglow action completes and their streak updates without entering a live room. Given multiple “check” reactions are sent by the same user, when idempotency is applied, then completion is recorded only once while aggregate counts may still reflect additional reactions. Given session records are persisted, when the Afterglow closes or at scheduled intervals, then only aggregate counts per reaction type are stored and no user-level reaction history is persisted.
Late Joiner 2-Hour Async Check-in Window
"As a late joiner, I want a two-hour window to check in asynchronously so that my streak is preserved within my rolling window."
Description

Allow users who missed the live Afterglow to submit a lightweight check-in within a two-hour asynchronous window aligned to their rolling window rules. Display the prompt and reaction set without the live room, record the check-in server-side, and update streaks atomically. Enforce eligibility: only users who attended the session or are on the invite list and are supporters can access this window. Use server timestamps to prevent backdating, deduplicate submissions, and handle retries idempotently. Provide one reminder notification within the two-hour window, respecting user notification preferences. Analytics capture: completion rate, latency to check-in, and impact on next-day adherence.

Acceptance Criteria
Eligibility Gate: Supporter + Attendee/Invite
Given a user opens the Afterglow async check-in entry point for a session When the server validates eligibility against authoritative records Then access is granted only if the user is a current supporter AND (attended the session OR is on the session’s invite list) And if the user is ineligible, the API responds 403 and no prompt/reactions are returned And eligibility is evaluated server-side; client-supplied eligibility signals are ignored
Time Window Enforcement with Server Time and Rolling Window
Given server timestamps for session_end_ts and user_rolling_window_end_ts When a user attempts an async check-in Then window_end_ts = min(session_end_ts + 2 hours, user_rolling_window_end_ts) And the submission is accepted only if server_now ∈ [session_end_ts, window_end_ts] And attempts outside this interval return 410 and no check-in is recorded And client-provided timestamps are ignored for eligibility; server_now is authoritative
Async Check-in UI Without Live Room
Given an eligible user opens the async check-in When the screen loads Then the session’s prompt text and predefined reaction set are displayed And no live room elements (video, chat, presence) are shown And the user’s identity is shown as a pseudonymous handle only And supporter badges are visible with subdued styling per design And the primary action allows a one-tap check-in with an optional single reaction selection
Atomic Streak Update on Async Check-in
Given a valid async check-in submission is received within the allowed window When the server processes the submission Then the check-in record is created and the user’s streak is updated in a single atomic transaction And at most one streak increment occurs for the user’s applicable rolling window And concurrent submissions from multiple devices do not create duplicate check-ins or multiple streak increments
Deduplication and Idempotent Retries
Given the client sends an idempotency_key scoped to user_id + session_id with each submission When duplicate submissions arrive with the same idempotency_key Then the server returns the original success response without creating a new record And if a check-in already exists for the user and session, subsequent submissions with different keys return 409 and do not alter state And transient network retries do not result in duplicate records or multiple streak increments
Reminder Notification Within Window Respecting Preferences
Given an eligible user has not completed the async check-in When within the two-hour window after session_end_ts and before window_end_ts Then exactly one reminder is scheduled and delivered according to the user’s notification preferences And no reminder is sent to users who have disabled Afterglow reminders or all notifications And if the user completes the check-in before the reminder fires, the reminder is canceled And no reminder is sent after window_end_ts And each reminder attempt is logged with status sent, suppressed, or canceled
Analytics: Completion, Latency, Next-Day Adherence
Given analytics collection is enabled When users become eligible and interact with the async check-in Then events are emitted for async_checkin_viewed, async_checkin_submitted, reminder_sent, and reminder_canceled with properties session_id, user_id_hash, and server timestamps And async_checkin_submitted includes latency_seconds = server_checkin_ts − session_end_ts And completion_rate per session/cohort is computable as completed/eligible from emitted events And next_day_adherence is computed as a boolean for the next rolling window and attributed to the async check-in cohort
Pseudonymous Display & Subtle Badge Shine
"As a privacy-conscious supporter, I want my pseudonym shown and my supporter badge to shine subtly in Afterglow so that I’m recognized without exposing my real identity."
Description

Render participant identities using pseudonymous handles and avatars only, suppressing real names and profile links inside Afterglow and its async window. Visually differentiate supporters with a subtle badge shine effect limited to the Afterglow context, respecting reduced motion and low-power settings. Ensure no badge or identity is exposed to non-eligible users and that screenshots or previews do not leak handles. Provide an opt-out for visual effects without removing supporter status. Theming and contrast must meet accessibility guidelines while keeping the effect tasteful and minimal. Cache-safe assets and animations to avoid frame drops on low-end devices.

Acceptance Criteria
Pseudonymous Identity Only in Afterglow and Async Window
Given a participant view is rendered in Afterglow live or its 2-hour async window When roster, reactions, and check-ins are displayed Then each participant shows only a pseudonymous handle and avatar; no real name, email, phone, or external/profile link appears or is tappable And any tap/long-press opens a mini-card with handle and avatar only and no navigation to a profile screen And network responses used by these UIs exclude real-name and profile link fields And clipboard copy and share actions are disabled for handles in this context And screen readers announce "handle, participant" and do not expose real identifiers
Context-Limited Supporter Badge Shine with Motion/Power Respect
Given a supporter is visible in an Afterglow live session When their badge renders Then a subtle shine animation is displayed only in Afterglow, not in lobby, home, or history And the animation meets: luminance delta <= 6%, duration 1.2–1.6s, max 4 cycles per minute, no color strobes, no scale > 1.0 And if OS Reduced Motion or app Reduce Effects is enabled or system Low Power Mode is on, the badge shows a static accent with zero motion And non-supporters show no shine or supporter styling And the shine never occludes or lowers adjacent text contrast below WCAG AA
Visual Effects Opt-Out Preserves Supporter Status
Given a supporter toggles "Disable visual effects" in Afterglow settings When they join Afterglow live or open the async window Then their supporter status remains visible via a static badge icon and accessible label "Supporter" And no shine animation or motion effect is rendered for them And the setting persists across app restarts and across devices for the same account within 5 minutes of change And re-enabling effects resumes shine without requiring rejoin or app restart
No-Leak Screenshots, Recents, and Link Previews
Given the user is in Afterglow live or the async window When the app goes to background or appears in the system recents/app switcher Then participant handles and avatars are blurred/redacted in the app snapshot; Android uses FLAG_SECURE; iOS uses a privacy overlay for snapshots And when a screen recording or capture is detected, handles and avatars are masked while capture is active and a non-sensitive toast is shown And any generated share images, room previews, or link unfurls replace handles with generic labels (e.g., "Member 1") and include no badge shine or pseudonyms And deep-link metadata contains no participant-identifying fields
Eligibility Gating and Redaction for Non‑Eligible Users
Given a non-eligible user (not in the Afterglow room or outside the window) accesses an Afterglow link or screen When the view loads Then no participant handles, avatars, or badges are fetched or rendered; a gated access screen is shown And API returns 403/unauthorized with redacted payload containing no participant identifiers And push notifications and in-app previews for non-eligible users contain no handles or badge indicators
Performance and Cache Safety on Low‑End Devices
Given a low-end device (2 GB RAM, 4x little cores, 60 Hz display) and a room of up to 100 participants with 10% supporters When rendering roster and badge effects for 60 seconds Then average frame time <= 16.7 ms, p95 frame time <= 24 ms, and <= 1 dropped frame per 5 seconds during idle and shine cycles And additional CPU utilization from badge effects <= 5% and GPU memory overhead <= 20 MB And all badge assets/animations are served from cache after initial load; no network requests for animations post-join And total additional memory for pseudonym/badge assets <= 30 MB and cold-start asset download for badge <= 200 KB
Accessible Theming and Contrast for Pseudonyms and Badge
Given light and dark themes and system high-contrast mode When pseudonymous handles and badges are displayed Then text contrast ratios meet WCAG 2.2 AA: >= 4.5:1 for handles and >= 3:1 for adjacent icons on both themes And keyboard focus order reaches each participant entry; focus outline contrast >= 3:1 And screen readers announce "handle, participant, supporter" for supporters and never read real names or external links And the shine effect does not trigger in high-contrast mode; a static high-contrast outline is used instead
Soft Upsell Teaser & Conversion Pathway
"As a non-supporter, I want a clear, non-intrusive path to join Afterglow so that I can decide to upgrade when the value is most relevant."
Description

For non-supporters at session end, display a gentle teaser card indicating that an Afterglow just opened, with anonymized metrics (e.g., “Supporters are reflecting now”) and a clear, frictionless upgrade CTA. Do not disclose room content, prompt text, or participant identities. Support one-tap upgrade, entitlement refresh, and immediate entry if completed within the live window; otherwise, enable access to the remaining async window. Respect user settings to suppress repeated prompts and cap frequency. Measure conversion lift and avoid interfering with core session flows via dismissible UI.

Acceptance Criteria
Teaser Visibility & Anonymization at Session End
Given a non-supporter completes a live session where Afterglow is enabled When the session end state is reached Then a teaser card appears within 500 ms of session end And the teaser shows only anonymized metrics (e.g., aggregate count or generic copy) And no prompt text, room content, avatars, handles, or participant names are displayed And if active participant count < 3, the teaser shows generic copy without any numeric counts And if the user is already a supporter or Afterglow is not enabled for the session, the teaser is not shown And the teaser is dismissible via a clear control and swipe, returning the user to the standard session summary view
One-Tap Upgrade, Entitlement Refresh, and Immediate Entry
Given a non-supporter sees the teaser and taps the upgrade CTA When the purchase flow completes successfully Then supporter entitlement is refreshed and reflected in the app within 3 seconds And the user is automatically routed into the live Afterglow micro-room if the live window is active And if the live window is not active, the user is routed to the Afterglow async check-in view with remaining time displayed And no additional taps are required after purchase completion And duplicate taps on the CTA are debounced so only one purchase flow is initiated within a 2-second window And if the purchase is cancelled or fails, an inline non-blocking error state is shown and no entitlement change occurs
Async Window Access After Live Afterglow Ends
Given the live Afterglow window has ended And a non-supporter upgrades within 2 hours of the session end time When the purchase completes successfully Then the user is routed to the Afterglow async check-in view And the remaining window duration is accurate within ±5 seconds And the user can complete a check-in within the remaining window And if the upgrade occurs after 2 hours have elapsed, the user sees a closed-state message and cannot access the room
Prompt Suppression & Frequency Capping
Given a user dismisses the teaser without upgrading When they complete subsequent sessions within the suppression period Then the teaser does not appear during that period And if the user selects "Don't show again", the suppression lasts 7 days (configurable via remote config) And otherwise, the teaser is capped at a maximum of 1 impression per user per 24 hours across sessions (configurable) And the teaser never reappears in the same session flow once dismissed And if the user has enabled a global setting to suppress upsell prompts, the teaser is never shown And suppression and caps persist across app restarts and devices via durable storage/server rules
Non-Blocking, Dismissible UI That Preserves Core Flow
Given the session completes When the teaser appears Then core completion actions (e.g., Continue/Done, navigation) remain accessible and unobstructed And dismissing the teaser completes within 100 ms and returns focus to the session summary And the teaser never displays while an active session timer is running And the teaser occupies no more than 75% of the viewport height on small screens And the teaser, CTA, and dismiss controls have accessible labels and meet WCAG AA color contrast (≥ 4.5:1) And no haptic/audio interruptions are triggered by the teaser
Analytics, Experimentation, and Privacy Compliance
Given a user is eligible for the teaser When the teaser is shown, clicked, a purchase succeeds/fails, or the user is routed to live/async entry Then analytics events fire for impression, click, purchase_success, purchase_failure, and entry_routed with fields: pseudonymous user_id, session_id, room_id, timestamp, variant_id, context (live|async) And events contain no PII, prompt text, or participant identifiers And events are dispatched within 2 seconds with at-least-once delivery and de-duplicated via event_id And experiment variants are assigned and sticky per user for 14 days and included in all events And conversion can be calculated by joining impression to purchase within a 24-hour attribution window

Support Boosts

Micro‑donations that trigger tasteful, shadow‑style reactions and advance a visible Support Meter toward creator goals (e.g., bonus session, template drop). Rate‑limited to avoid spam and fully compatible with Ghost Cred and Avatar Veil. Benefits: instant feedback for supporters, incremental earnings for creators, and room energy lifts without calling anyone out by name.

Requirements

One-Tap Boost Purchase
"As a supporter, I want to send a quick boost with one tap so that I can encourage the creator without interrupting my session."
Description

Implements a frictionless, in-room micro-donation flow with preset amounts and a single-tap confirmation that immediately registers a Boost, issues a receipt, and updates the session ledger without revealing donor identity. Supports compliant payment methods, localized currencies, tax/fee handling, error states (fail, retry, duplicate), and instant visual confirmation. Integrates with Support Meter progression, shadow reactions, and payout reporting while minimizing latency to preserve session momentum.

Acceptance Criteria
Single-Tap Boost With Preset Amounts
Given I am in a live room with Boosts enabled and preset amounts are available in my localized currency And supported payment methods on my device are available (e.g., Apple Pay, Google Pay, card-on-file) When I tap a preset amount and confirm with a single tap Then the payment is confirmed without additional screens or text entry And the UI acknowledges my tap within 100 ms with a pressed/processing state And the end-to-end confirmation event is received on my client within 800 ms in 95% of cases And preset amounts respect server-configured min/max and show correct currency symbol and formatting
Immediate Boost Registration and Session Ledger Update
Given the payment is confirmed When the Boost is recorded Then a Boost record with unique id, amount, currency, and timestamp is appended atomically to the session ledger And my client displays the new ledger entry within 800 ms; other room participants see it within 1.2 s And the Boost is persisted durably and replays on reconnect within 5 s if the client was offline And the payout reporting pipeline receives the transaction within 5 minutes with correct amount, currency, fees, and taxes
Receipt Issuance and Fiscal Handling
Given a Boost succeeds Then an in-app receipt is shown within 1 s with amount, currency, fees/tax breakdown, timestamp, and transaction id And an email receipt is delivered within 60 s when email consent exists And taxes and fees are calculated per locale and payment-method rules and displayed with correct currency formatting And currency conversions and rounding match provider settlement amounts within ±0.01 minor units And no donor-identifying information appears on any receipt visible to the room or creator
Donor Anonymity and Privacy Preservation
Given a Boost is successful Then no username, avatar, email, or device identifiers are displayed in the room UI, reactions, meter, ledger, or creator-facing views And analytics and logs store only pseudonymous donor identifiers; no PII is emitted in real-time events And payout and session reports aggregate Boosts without linking to specific user identities And Ghost Cred and Avatar Veil modes remain intact; no UI element reveals identity when those modes are active And QA verifies that screen recordings and logs redact identifiers
Rate Limiting and Duplicate Prevention
Given a user is in a room When the user attempts multiple Boosts rapidly Then a per-user per-room cooldown enforces max 1 successful Boost per 10 seconds And duplicate taps within 5 seconds reuse the same idempotency key and do not create additional charges or ledger entries And a room-level throttle limits to 10 Boosts per 10 seconds; excess requests receive HTTP 429 with a Retry-After header And the UI shows a non-blocking cooldown message within 200 ms when limits are hit
Error Handling and Retry
Given a Boost attempt fails due to decline, network error, or timeout Then the user sees a clear error with reason category (Declined, Network, Timeout) and a single-tap Retry option And failed attempts do not capture funds and do not create ledger entries And retries within 2 minutes reuse the same idempotency key to prevent duplicates And if payment succeeds but the client disconnects, on reconnection a single successful Boost is shown with receipt and no duplicate charge And telemetry records an error with a correlation id and no PII
Support Meter Progression and Shadow Reactions
Given a Boost succeeds Then the Support Meter increments according to configured conversion rules for the room goal And a shadow-style reaction animation plays for 1.5–2.0 seconds with no donor identification And first visual feedback appears on the supporter’s client within 300 ms of confirmation and on other clients within 1 s And meter values are consistent across clients within ±1 unit within 2 s And visual/auditory feedback respects user Focus/Silent settings
Shadow Reaction Effects
"As a participant, I want boost reactions to feel energizing yet unobtrusive so that the room stays focused and inclusive."
Description

Delivers tasteful, ambient visual and haptic reactions triggered by Boosts that uplift room energy without calling out donors by name. Effects adapt to room theme and performance constraints, respect reduced-motion/accessibility settings, and coalesce during bursts to avoid visual spam. Fully compatible with Avatar Veil to maintain anonymity and instrumented for engagement metrics. Optimized for low-latency rendering across mobile and web clients.

Acceptance Criteria
Boost triggers ambient shadow reaction without attribution
Given a Boost is sent in a live room with Avatar Veil enabled When the client receives the boost event Then an ambient shadow-style visual effect is rendered within 150 ms of receipt And no donor name, avatar, or unique identifier is displayed in UI or exposed via accessibility labels or logs And the effect contains no text overlays or callouts that imply donor identity And an analytics event ReactionShown is emitted with fields {roomId, reactionId, boostId, veil:true, ts}
Reduced-motion and haptic-respect behavior
Given a user has OS/app Reduce Motion enabled or device haptics disabled/do-not-disturb active When a Boost is received Then render the low-motion variant (opacity fade ≤ 300 ms, no particle/physics) and suppress all haptics And emit ReactionSuppressed with {reason:"reduce_motion"} And the Support Meter still updates as normal without additional animation Given no reduced-motion setting and haptics are available When a Boost is received Then play a light impact haptic (platform standard) synchronized within ±50 ms of first frame
Burst coalescing and reaction frequency cap
Given multiple Boosts arrive within a 3-second window When rendering reactions Then coalesce them into a single composite effect whose intensity scales with count (1–5+ tiers) And enforce a max display frequency of one visual effect every 800 ms per room And limit to at most 3 composite reactions within any rolling 10-second window And emit ReactionCoalesced with {windowMs:3000, boostCount, compositeId} And no individual-donor visuals are queued while coalescing is active
Low-latency, smooth rendering across mobile and web
Given a mid-tier device (mobile and web reference profiles) When a Boost is received Then time-to-first-reaction-frame (TTFR) is ≤ 150 ms at P95 and ≤ 220 ms at P99 And animation maintains ≥ 55 FPS at P95 for its duration with dropped frames ≤ 1% P95 And main thread long tasks > 50 ms during the effect are 0 at P95 And memory spikes attributable to the effect are ≤ 20 MB on mobile and ≤ 30 MB on web at P95
Theme-adaptive, tasteful visuals
Given the room theme is Light, Dark, or Custom with defined token palette When rendering a shadow reaction Then colors derive from theme tokens (accent-2/background-elevated) and respect contrast and alpha rules (max opacity 0.35) And glow/blur radii and durations select the theme’s scale (S/M/L) without overriding global style rules And on missing tokens, fallback to neutral palette without blocking render And screenshots show no color banding or illegible overlays across themes
Telemetry and privacy guarantees
Given reactions are rendered, suppressed, or coalesced When emitting analytics Then the following events are captured at 100% sample: ReactionQueued, ReactionShown, ReactionSuppressed, ReactionCoalesced, ReactionError And each event includes {roomId, platform, clientVersion, ts, reason?} and excludes donor identifiers and PII And logs do not contain donor identity even in debug builds And metrics dashboards report daily counts and P95 TTFR with data latency < 5 minutes
Support Meter Progression
"As a creator, I want a clear meter that advances with boosts so that supporters can see progress toward my next goal."
Description

Displays a real-time, visible meter that advances with each Boost according to configurable amount-to-progress rules, reflecting progress toward creator-defined goals (e.g., bonus session, template drop). Persists across sessions, synchronizes state for all participants, supports deadlines and multi-goal queues, and emits a neutral, non-attribution announcement on goal completion. Handles resets, partial completions, and offline reconciliation to ensure consistency.

Acceptance Criteria
Real-Time Meter Advancement and Synchronization
Given a live room with an active goal and connected participants When a supporter sends a valid Boost of amount A and the server acknowledges it Then the Support Meter advances by f(A) per the active mapping rules without exceeding the active goal’s remaining capacity And all connected clients display the updated meter value within 500 ms at the 95th percentile from server acknowledgment And concurrent Boosts are applied in server event-time order producing a single, consistent meter value across all clients And the displayed meter value and server value match exactly (tolerance = 0)
Configurable Amount-to-Progress Mapping
Given a creator-defined mapping ruleset R1 is active When the creator updates the mapping to R2 via settings and confirms Then subsequent Boosts use R2 while existing accumulated progress remains unchanged And the ruleset change is audit-logged with timestamp, actor, and rule diff And client UIs reflect the new rule label/description within 2 seconds And invalid mappings (e.g., negative rates, non-monotonic steps) are rejected with a descriptive error and no partial application
State Persistence Across Sessions
Given the Support Meter server state is V at time T When a participant force-quits the app and reopens the same room Then the meter initializes from the server and displays the latest authoritative value (>= V accounting for any intervening events) And no additional progress is applied from local caches upon reload (no double-counting) And the last-updated timestamp shown to the client is within 100 ms of server time And a cold load to first rendered meter completes within 1.5 seconds on a 4G connection
Deadline Expiry Handling and Reset
Given an active goal G1 with deadline D and current progress P% < 100% When the current time reaches D Then G1 is marked expired, progress for G1 is archived, and the visible meter resets to 0% for the next queued goal (if any) And a neutral, non-attribution expiry announcement is emitted once across all clients within 1 second And no carry-over progress is applied from G1 to the next goal upon deadline expiry And all clients converge to the same new active goal within 500 ms at the 95th percentile
Multi-Goal Queue and Partial Completion Carryover
Given a queued set of goals [G1, G2, G3] with G1 active and remaining capacity C1 When a single Boost of amount A exceeds C1 Then G1 reaches 100% and emits exactly one neutral completion announcement And the excess (A - C1) is applied to G2 using the active mapping, possibly completing multiple goals in order And a maximum of one announcement per completed goal is emitted in sequence order And the final active goal and meter value reflect the exact residual after sequential application, matching server totals
Neutral Goal Completion Announcement (Non-Attribution)
Given an active goal reaches 100% progress When the completion is processed by the server Then a system announcement is broadcast that includes the goal title and completion status only And the announcement contains no supporter names, avatars, amounts, or identifiers, complying with Ghost Cred and Avatar Veil And the announcement appears for all connected clients within 1 second of server completion and is identical across clients And screen reader text for the announcement is provided and passes WCAG AA for contrast and labeling
Offline Reconciliation and Idempotency
Given a participant is offline while multiple Boosts occur from other users When the participant reconnects Then the client fetches and applies the authoritative event stream since the last checkpoint, producing a meter value identical to the server And duplicate, late, or out-of-order events do not change the final meter (idempotent application) And any Boosts submitted by the offline client are retried with idempotency keys and are not double-counted if already processed And reconciliation of up to 500 events completes within 3 seconds on a 4G connection at the 95th percentile
Boost Rate Limiting
"As a moderator, I want boosts to be rate-limited so that the room energy stays positive without spam."
Description

Prevents spam and fatigue by enforcing per-user, per-room, and server-level rate limits on Boost submission and reaction display. Implements server-side validation, burst coalescing, exponential backoff, and client UI states that communicate cooldown timers. Provides configurable policy controls, observability, and safeguards against automation and abuse to maintain a healthy room experience.

Acceptance Criteria
Per-User Per-Room Boost Cooldown
Given an authenticated user is in a live room with a per-user limit L within window W When the user submits more than L Boosts within W Then the first L Boosts are accepted (201) and advance the Support Meter by the correct amount And subsequent Boost attempts within W are rejected with 429 BoostRateLimited including {remaining_ttl_ms, limit: L, window_ms: W} And the client disables the Boost control and shows a countdown equal to remaining_ttl_ms (±100ms) And when remaining_ttl_ms elapses, the Boost control re-enables and the next Boost is accepted And duplicate submissions with the same idempotency_key within 60s return 200 with {idempotent: true} and do not increase counts
Room-Level Reaction Display Throttle
Given a room has a display cap D reactions/second and burst coalescing is enabled When total Boosts across users exceed D per second Then no more than D reaction animations are rendered per second And excess Boosts within a 2s coalescing window are aggregated into a single shadow-style reaction with a visible counter And the Support Meter increments reflect all Boosts received (rendered + coalesced) And no supporter identities are revealed in the coalesced display
Server-Level Surge Protection and Exponential Backoff
Given a server-level global threshold G Boosts/second over a 5s rolling window When incoming Boosts exceed G Then a proportionally throttled subset of requests receive 429 with {reason: "global_limit", retry_after_ms} And clients apply exponential backoff (1s, 2s, 4s, 8s, … capped at 60s) before retrying And the client shows a global cooldown UI state during backoff and prevents retry until the backoff elapses And successful requests maintain P95 latency ≤ 200ms under load test at 1.5×G
Clock-Safe Cooldown Timers
Given client device clocks may be skewed When the server returns remaining_ttl_ms on a rate-limited response Then the client countdown derives solely from remaining_ttl_ms, not local time, and completes within ±250ms of the server TTL And attempts to Boost before TTL expiry are blocked with 429 including an updated remaining_ttl_ms And upon TTL expiry, the next Boost succeeds without requiring an app restart or room rejoin
Policy Configuration and Per-Room Overrides
Given an admin updates rate-limit policy with {per_user_L, window_W, room_display_cap_D, global_G} When the change is saved Then validation enforces ranges: 1≤L≤50, 1s≤W≤3600s, 1≤D≤100, 10≤G≤10000 And per-room overrides take precedence over global defaults And the effective policy propagates and is applied within 60s And a read-only GET /limits endpoint reflects the active policy for the room And an audit log entry is written with actor, scope, old→new values, and timestamp
Observability, Metrics, and Alerting
Given production traffic and rate limiting enabled When Boosts are processed, limited, or coalesced Then metrics are emitted: boosts_total, boosts_accepted, boosts_rejected{reason in [per_user, room_display, global_limit, suspected_automation]}, rate_limit_hits{scope}, coalesced_bursts, reaction_renders, support_meter_delta And structured logs include request_id, room_id, user_id_hash, limit_scope, decision, and ttl_ms (no raw PII) And dashboards show per-room and global QPS, 429 rate, coalescing rate, and P95/P99 latency And alerts trigger when 429 rate > 5% for 5m or P95 latency > 300ms for 5m
Abuse Safeguards and Identity Safety
Given patterns indicative of automation or abuse (e.g., >3 Boosts/sec/user, repeated idempotency keys, mismatched device fingerprints) within a 15m window When such patterns are detected Then the system elevates the user/session to a stricter backoff tier and responds with 429 {reason: "suspected_automation", remaining_ttl_ms} And blocked Boosts do not trigger reaction animations or Support Meter increments And Ghost Cred and Avatar Veil compatibility is preserved: messages and UI do not reveal supporter identities or attribution And all actions are audit-logged with minimal identifiers (e.g., user_id_hash)
Creator Goal & Reward Setup
"As a creator, I want to configure goals and rewards so that supporters know what their boosts are unlocking."
Description

Provides creators with tools to configure Support Meter goals, including target amounts, descriptions, deadlines, and attached rewards such as bonus session scheduling or content/template drops. Supports draft and publish workflows, safe edits with audit trails, automated reward fulfillment on goal completion (e.g., unlock link, send RSVP), and compliance checks. Surfaces goal context in-room so supporters understand what their Boosts unlock.

Acceptance Criteria
Publish Valid Goal with Reward
Given I am a creator on the Goal Setup screen And I have entered a target amount in USD between 5 and 10000 And I have entered a goal title (max 60 characters) and description (max 200 characters) And I have set a deadline between 1 and 90 days from now in my workspace timezone And I have attached at least one reward When I tap Publish Then the goal status changes from Draft to Published And the goal appears in my room’s Support Meter within 2 seconds And invalid fields block publish with inline error messages and no goal is created
Configure Reward Types
Given I am attaching rewards to a goal in Draft When I select Bonus Session reward Then I must set session duration (15–120 minutes), scheduling window (3–30 days), and capacity (1–500) And the reward summary displays "Bonus session" with duration and window When I select Content/Template Drop reward Then I must provide a title (max 60 characters) and a locked delivery URL or file (up to 200 MB) And the reward summary displays "Content drop" with title And at least one reward is required to publish
Automated Reward Fulfillment on Goal Completion
Given a goal with Content/Template Drop reward is Published And the Support Meter reaches or exceeds the target before the deadline When the target is reached Then the system unlocks the delivery link to all supporters who boosted during the goal window within 10 seconds And sends an in-app notification to eligible supporters without exposing supporter identities And records a fulfillment entry with timestamp, goal ID, reward type, and count of recipients Given a goal with Bonus Session reward is Published And the Support Meter reaches the target before the deadline When the target is reached Then the system sends an RSVP scheduler to eligible supporters within 10 seconds And prevents double-sending on retries via an idempotent operation
Safe Edits and Versioned Audit Trail
Given a goal is Published When I attempt to edit Then I can edit only title, description, and deadline (not target amount or reward type) And any deadline edit must remain ≥ now and ≤ 90 days from publish And the system creates a new version capturing editor ID, timestamp, and changed fields And the audit trail shows all versions in reverse chronological order And edits trigger re-validation and compliance checks before applying Given any edit fails validation or compliance When I save Then no changes are applied and I see specific inline errors
Compliance Validation on Publish and Edit
Given I have a goal in Draft or I am editing a Published goal When I run validation explicitly or on Publish/Save Then the system denies save/publish if any rule fails: - deadline must be a future date within 90 days - reward links must be HTTPS and on the allowed domains list - no prohibited terms from the policy dictionary appear in title/description - target amount is within 5–10000 USD And I see field-level error messages listing failed rules And the system logs a compliance decision with rule IDs and outcome
In-Room Goal Context Display
Given a goal is Published When a supporter opens the room Then the Support Meter card displays goal title, target amount, current progress (amount and %), reward summary, and deadline countdown And no supporter names or avatars are displayed in this context, respecting Ghost Cred and Avatar Veil And the display updates within 1 second after a new Boost is applied And the content is accessible on iOS, Android, and Web with consistent layout
Goal Expiry and Non-Attainment
Given a Published goal reaches its deadline without meeting the target When the deadline passes Then the goal auto-transitions to Closed with status "Not Met" And no reward fulfillment is triggered And the Support Meter card shows "Goal ended" with final progress for at least 24 hours And the creator can duplicate the closed goal to a new Draft with one tap
Ghost Cred & Veil Compatibility
"As a privacy-conscious supporter, I want my boost to count toward my Ghost Cred while keeping my identity veiled so that I can contribute safely."
Description

Ensures Boosts contribute to a supporter’s Ghost Cred while preserving anonymity via Avatar Veil. Aggregates and displays anonymous supporter counts and contribution impact without revealing identities in UI, logs, or exports. Provides optional private confirmations to donors and enforces privacy-by-default across reactions, meter updates, analytics, and receipts.

Acceptance Criteria
Private Ghost Cred Accrual Under Avatar Veil
Given a logged-in supporter with Avatar Veil enabled and Ghost Cred balance G And they are in a live room with Support Boosts enabled When they send a Boost of amount A Then their Ghost Cred increases by Δ=f(A) per current configuration And the updated Ghost Cred is visible only in their private profile/ledger And no username, handle, or avatar is shown on any public surface (live reactions, room feed, Support Meter tooltip, notifications) And the visible reaction uses a veiled, non-identifiable avatar
Anonymous Aggregates in Support Meter and Room UI
Given a room receives N Boosts within a session When any participant views the Support Meter, supporter count, or contribution impact Then the UI displays aggregated totals (count, sum, and progress toward goal) only And no supporter identifiers or clickable elements link to a profile And if N < 3, the UI labels "anonymous supporter(s)" and omits per-Boost timestamps/breakdowns to prevent re-identification
Privacy-by-Default Across Analytics, Logs, and Exports
Given Boost events are generated When system logs, analytics dashboards, CSV exports, and webhooks are reviewed Then no fields include supporter identifiers (name, handle, email, userId, deviceId, IP, payment token) And only event-level anonymized IDs and aggregated metrics are exposed And any attempted inclusion of PII is redacted and blocked by schema validation tests And error/trace logs exclude request bodies containing donor PII
Optional Private Confirmation to Donor Only
Given a supporter has "Private Boost Confirmations" enabled When they send a Boost Then they receive a private in-app receipt (amount, timestamp, Ghost Cred Δ, Support Meter change) And no public or creator-facing surface reveals that this specific supporter sent the Boost And if confirmations are disabled, no receipt is sent while the Boost, meter update, and Ghost Cred accrual still occur
Rate Limiting Preserves Anonymity and Prevents Spam
Given the system rate limit is L=5 Boosts per supporter per room per 60 minutes When a supporter sends up to L Boosts in that window Then Support Meter and aggregates update cumulatively without any identity exposure or per-user grouping And the UI avoids repetitive reaction patterns tied to a single supporter And when L is exceeded, the supporter receives a private error message and no public indicator is shown
Creator/Admin Views Are Aggregated-Only
Given a creator opens room analytics or exports When filtering, segmenting, or exporting Support Boosts data Then only aggregated counts, totals, and trends are available And no donor lists, per-user histories, or identifiers can be accessed or exported And attempts to access raw donor identities are blocked and logged in an authorization audit without exposing identities
Boost Analytics & Payouts
"As a creator, I want clear analytics and payout reports so that I can understand performance and plan future goals."
Description

Delivers analytics for creators and the platform, including Boost counts, revenue, conversion by session, reaction engagement, and impact on streak adherence, alongside payout reporting for earnings, fees, pending, and paid totals. Supports filtering by time range and room, CSV export, and privacy-preserving aggregation. Correlates events with Support Meter milestones to inform goal strategy.

Acceptance Criteria
Time-Range and Room Filtering on Boost Analytics Dashboard
Given a creator has Boost events across multiple rooms and dates When the creator selects a start date, end date, and an optional room filter Then the dashboard recalculates Boost count, unique supporters, gross revenue, platform fees, net revenue, reaction count, conversion by session, and Support Meter progress for the filtered scope And all displayed totals equal the sum of underlying events in the filtered scope And the creator’s configured timezone is used for all date boundaries and timestamps And results load within 1500 ms for up to 50,000 events in scope And clearing filters returns the view to the default last-30-days, all-rooms state And an empty result set displays zeros and a “No data for selected filters” message
CSV Export of Boost Events and Payouts
Given filters are applied on the analytics dashboard When the creator clicks Export CSV for Boost events Then a CSV is generated within 10 seconds containing columns: event_id, timestamp_iso, room_id, session_id (nullable), supporter_hash, amount_cents, currency, platform_fee_cents, net_amount_cents, reaction_type, support_meter_milestone_id (nullable) And only events within the current filters are included And timestamps are ISO 8601 with timezone offset And the file name follows pattern: boosts_{room-or-all}_{startDate}_{endDate}.csv And no PII (names, emails, avatars) is included; supporter_hash is a stable, one-way hash When the creator clicks Export CSV for Payouts Then a CSV is generated containing columns: payout_id, payout_date, status, gross_amount_cents, fees_cents, net_amount_cents, currency, transfer_reference And payout totals reconcile to on-screen totals for the selected date range
Payouts Ledger Summary and Reconciliation
Given Boost earnings have accrued and payment processing is enabled When the creator opens the Payouts view for a selected date range Then the view shows Gross Earnings, Platform Fees, Processing Fees, Net Earnings, Pending, and Paid totals And totals equal the sum of underlying Boost events and payout transactions in the selected range And each payout batch displays payout_id/reference, currency, amount, status (Pending/Processing/Paid/Failed), initiated_at, paid_at (nullable) And currency amounts are rounded to two decimals and match the payment processor to the cent And when a payout transitions from Pending to Paid, the Paid total increases and Pending decreases by the same net amount And failed payouts are excluded from Paid totals and surfaced with an error badge
Session Conversion and Reaction Engagement Metrics
Given sessions record attendance and Boost events When computing per-session metrics for a filtered range Then Conversion Rate per session = unique supporters who boosted in the session / unique attendees of that session, expressed as a percentage with one decimal And Reaction Engagement per session = count of shadow reactions triggered by Boosts in that session, rate-limit respected And a supporter contributes at most one conversion per session (deduplicated by supporter_hash) And sessions with fewer than 5 attendees are labeled “Insufficient sample” and excluded from aggregate conversion calculations And per-session rows plus aggregates match underlying event counts
Streak Adherence Impact Correlation
Given supporters have check-in histories and some made Boosts in the selected range When calculating adherence impact Then for each supporter, compute 7-day adherence rate before vs. after their first Boost in range (requires at least 4 days observed in each window) And the dashboard displays cohort-size, mean delta adherence (post minus pre), and a distribution summary (p25/median/p75) And results are aggregated with no individual-level output and require a minimum cohort size k=20; otherwise display “Insufficient data to protect privacy” And supporters with overlapping boosts within the 14-day window are still counted once using the first Boost as the anchor And aggregate values equal the mean of individual deltas within the cohort
Support Meter Milestone Correlation Analytics
Given Support Meter milestones are configured and milestone-reached events are logged When viewing milestone analytics for a filtered range Then each milestone shows: milestone_id, goal label, reached_at, boosts-to-reach (count), revenue-to-reach, time-to-reach (first boost to milestone), and revenue between milestones And the view reports conversion lift: average session conversion in the session immediately after the milestone vs. the session immediately before And milestone associations for Boost events are derived from the active meter state at event time And totals and lifts are computed only when sample size for each side ≥ 5 sessions; otherwise mark as “Insufficient sample”
Privacy-Preserving Aggregation and Avatar Veil/Ghost Cred Compatibility
Given analytics may be viewed at room and session levels When rendering any metric or exporting CSV Then no supporter names, avatars, emails, or direct identifiers are displayed or exported And supporter identifiers in analytics/exports are irreversible hashes with stable salt per creator account And any breakdown (room, session, device, geography) requires k-anonymity with k≥10; sub-k results are suppressed or bucketed into “Other” And when Avatar Veil or Ghost Cred modes are enabled, analytics continue to function without revealing identity or de-anonymizing supporters And privacy rules apply equally to creators and platform-level views

Smart Doors

Automated room access that opens early for passholders, enforces capacity, and manages a waitlist with auto‑invites when seats free up. Charges are captured only when seated; no‑shows auto‑credit a future session. Clear door states appear on the Window Timeline so users know when to hop in. Benefits: less admin for creators, fair access for supporters, and fewer last‑minute scrambles that risk streaks.

Requirements

Capacity Enforcement & Seat Reservation Engine
"As a participant, I want capacity to be enforced and seats reserved fairly so that entry is orderly and rooms remain focused."
Description

Provide a robust, atomic seat management service that enforces per-room capacity limits and assigns seats on entry. Implement a clear seat state machine (available, held, seated, released, forfeited) with short-lived holds and grace periods to prevent overbooking and ghost seats. Handle high-concurrency joins with optimistic locking or distributed mutexes, guaranteeing one seat per user and idempotent re-tries. Emit real-time capacity and seat changes to clients, and persist an auditable log for dispute resolution. Integrate with StreakShare’s session lifecycle so capacity gates apply consistently across live rooms and micro-commitment windows.

Acceptance Criteria
Concurrent Joins Respect Room Capacity
Given a room with capacity C and 0 occupied seats When N ≥ C concurrent join requests are submitted within 100 ms Then the number of seated users never exceeds C at any moment And exactly C users transition available -> held -> seated And the remaining N - C requests receive capacity_full with no seat record created And no two seated users share the same seatId And the audit log shows no overlapping assignments for any seatId
Seat Hold TTL and Grace Period Enforcement
Given hold_ttl = 10s and grace_period = 30s When a user receives a held seat and does not confirm entry within hold_ttl Then the seat transitions held -> forfeited and capacityRemaining increases by 1 And a SeatStateChanged event is emitted with from=held, to=forfeited And no seated record exists for that user Given a seated user disconnects and no heartbeat is received for grace_period When the grace_period elapses Then the seat transitions seated -> released and capacityRemaining increases by 1 And a SeatStateChanged event is emitted with from=seated, to=released
Idempotent Retries and Duplicate Join Protection
Given a join request includes idempotency_key K for user U and room R When the same request with K is retried up to 5 times within 60s Then the response contains the same seatId and state as the first successful attempt And no additional seat transitions are logged for U in R And U ends up with at most one seat in any state Given two concurrent join requests for U use different idempotency keys When both reach the service Then at most one request succeeds in creating/returning a seat and the other returns already_processing or already_seated without increasing occupancy
Real-Time Seat and Capacity Events Emission
Given a client is subscribed to room R seat-capacity channel with lastEventId = L When any seat state changes or capacityRemaining changes in room R Then the client receives an event within 500 ms of the change being persisted And each event includes roomId, seatId, userId, fromState, toState, capacityRemaining, occurredAt (UTC ISO8601), requestId, sequence And events are delivered in ascending sequence order per room without duplicates And on reconnect providing lastEventId = L, the client receives all missed events L+1..current before new live events
Auditable Seat Transition Log
Given a room session with multiple joins, leaves, and expirations When the seat transition log is queried by roomId Then it returns a time-ordered, immutable list of entries containing seatId, userId, fromState, toState, occurredAt (UTC ISO8601), requestId, actor, reason And each seat’s transitions conform to the state machine: available -> held -> seated -> released|forfeited And at no point do parallel log entries imply more than capacityRemaining seats available or total seated > capacity And log entries are append-only; update/delete operations via public APIs are rejected
Session Lifecycle Capacity Gating (Live Rooms and Windows)
Given a session (live room or micro-commitment window) is in pre_open When a join request arrives Then no seat is held or seated and the client receives door_closed Given the session transitions to open When join requests arrive Then holds and seat assignments are permitted and capacity is enforced per rules Given the session transitions to closed or ended When seats are in held state Then they transition held -> forfeited and capacityRemaining is updated And any seated seats transition seated -> released And corresponding events are emitted for each transition
One Seat Per User per Room Across Devices
Given user U is already seated in room R When U attempts to join R from another device Then the service returns already_seated with the existing seatId and does not allocate an additional seat Given user U holds a seat but is not yet seated When U attempts a parallel join Then the same held seatId is returned and hold_ttl is not extended beyond policy Given user U’s seat is released after disconnect beyond grace_period When U attempts to rejoin Then U is treated as a new join and capacity is enforced
Intelligent Waitlist with Auto-Invites
"As a user, I want to join a waitlist and receive auto-invites when a seat opens so that I don’t need to keep checking the room."
Description

Introduce a waitlist queue that activates when capacity is reached, supporting FIFO with configurable creator rules (e.g., passholder priority within fairness bounds). When a seat is freed (via release, timeout, or exit), automatically issue time-boxed invites to the next eligible users with multi-channel notifications (push, in-app, email/SMS optional). Provide visible waitlist position, countdowns, and one-tap accept/decline flows. Handle expired invites with automatic re-queueing and backoff to avoid invite storms. Ensure resilience at scale and log outcomes for analytics (conversion, time-to-seat) and abuse prevention (rate limits, bot defense).

Acceptance Criteria
FIFO Queue with Passholder Priority Fairness
Given a room has reached capacity and waitlist is enabled And creator settings are priority.passholders=true, fairness.max_skips=1, fairness.window=10m And the current join order is [U1(non-pass), U2(pass), U3(non-pass)] When the next seat becomes available Then the system selects U2 first, then U1, then U3, honoring FIFO with at most one non-passholder skipped within the 10m window And if priority.passholders=false, selection strictly follows FIFO without reordering And each selection decision is logged with reason codes and the candidate pool snapshot
Auto-Invites on Seat Freed Events
Given a room is at capacity and a seat becomes available due to one of: (a) a seated user exits, (b) the host releases a seat, or (c) a hold/timeout expires When the event is recorded Then the system issues invites equal to the number of newly available seats within 3 seconds p95 to the next eligible users And each invite has a TTL configured by the creator (default 90s, range 30–180s) And active invites for a room never exceed the number of free seats And processing is idempotent; reprocessing the same seat-freed event does not create duplicate invites
One-Tap Accept/Decline and Atomic Seating
Given a user receives an active invite before TTL expiry When the user taps Accept via push deep link or in-app Then a seat is atomically assigned and the user moves from waitlist to seated within 1 second p95 And payment is authorized/captured only upon seat assignment; no charge occurs on invite send or decline And duplicate Accept actions within 2 seconds are idempotent and do not create extra seats or charges And if no seat is available at Accept time due to race, the user is shown 'Seat just taken' and remains on waitlist at their prior position When the user taps Decline Then the invite closes and the user is placed back on the waitlist with a backoff flag applied per configuration
Expired Invites, Re-Queue, and Backoff
Given an invite expires without response Then the user is automatically re-queued with a cooling_off backoff of at least 2 minutes (configurable 2–10 minutes) And during cooling_off the user is ineligible for selection and will be skipped without altering their numeric position counter And the user is not re-invited for the same room until backoff elapses And a user never has more than one active invite per room And all skip events and backoff windows are logged; p95 time from expiry to re-eligibility is within backoff + 5 seconds
Real-Time Waitlist Position and Invite Countdown Visibility
Given a waitlisted user is viewing the Window Timeline or room screen When any event affects their effective position or eligibility (join/leave, seating, skip due to backoff) Then the displayed position updates within 1 second p95 and never shows a negative value And upon invite issuance a countdown timer appears, accurate to ±1 second and synchronized to server time And Accept and Decline actions are visible and enabled only while time remains; they disable immediately on expiry or after one selection
Multi-Channel Invite Notifications with Preferences and Fallback
Given user notification preferences define enabled channels (push, in-app mandatory; email/SMS optional) When an invite is issued Then an in-app banner appears within 1 second p95 and a push notification is delivered within 3 seconds p95 And if enabled, SMS is delivered within 15 seconds p95 and email within 60 seconds p95 And all messages contain a deep link into the accept/decline flow that opens within 2 seconds p95 And if push delivery fails (no token/error), enabled SMS/email is sent as fallback within 5 seconds of failure And delivery and open statuses are logged per channel with timestamps and status codes
Capacity Enforcement, Abuse Prevention, and Analytics
Given platform load of ≥10,000 waitlisted users across ≥500 rooms When seats free and invites are processed Then invite selection latency is ≤200 ms p95 and seated count never exceeds configured capacity (no overbooking) And users are rate-limited to ≤3 invites per room per rolling hour and ≤1 Accept attempt per 2 seconds; violations return 429 and are logged And join/leave toggles >5 within 60 seconds trigger a soft challenge and deprioritization for 10 minutes And for every invite the system logs: invite_id, room_id, user_id, invited_at, ttl_s, channels_sent, delivered_at[], responded_at, outcome, time_to_seat_ms (if seated), and reason codes; records are queryable within 5 seconds And daily analytics expose conversion rate and median time-to-seat per room/creator via dashboard and export
Seated-Only Charging & Payment Safeguards
"As a supporter, I want to be charged only when I’m seated so that the process feels fair and transparent."
Description

Charge users only upon transition to the seated state, with optional pre-authorization at join to reduce payment failures. Implement idempotent billing operations, retry policies, and graceful failure handling (e.g., seat release on hard decline with immediate re-invite to waitlist). Provide instant receipts, ledger entries, and integration with StreakShare’s wallet/credits. Support multi-currency pricing, tax/VAT flags, and regional compliance. Expose clear status to users (pending, captured, failed) and to creators in analytics. Ensure PCI-compliant tokenization via payment provider and protect PII.

Acceptance Criteria
Charge Capture Only On Seating Transition
Given a user has joined a session and holds a valid payment method When the user transitions from waiting to seated Then the system captures the net session amount (price + applicable tax − applied wallet/credits) exactly once And the user’s payment status updates to "Captured" within 3 seconds And no capture occurs if the user exits before becoming seated And the creator dashboard reflects the captured revenue and seat utilization within 10 seconds
Optional Pre-Authorization at Join
Given the creator has enabled pre-authorization for the room And a user taps Join with a tokenized payment method When the join request is accepted Then the system creates a pre-authorization hold for the net expected amount (price + applicable tax − eligible wallet/credits) within 2 seconds And the user’s payment status shows "Pending" with visible amount and currency And if pre-authorization fails, the user is placed on the waitlist and prompted to update payment without losing their join position timestamp
Idempotent Billing and Safe Retries
Given a capture request is issued for a seated user When duplicate requests or webhook retries occur due to network or timeout Then the same idempotency key is used and only a single charge is created And the ledger contains one immutable charge entry with a unique transaction ID And subsequent duplicate events are logged and do not alter financial totals or user-visible status
Soft Decline Automatic Retry
Given a capture attempt returns a soft decline or transient error When the retry policy executes Then the system retries up to 3 times with exponential backoff over a maximum of 5 minutes And the user’s status remains "Pending" during retries with an explanatory banner And if a retry succeeds, the status switches to "Captured" and the seat remains assigned And if all retries fail before the retry window ends, the status switches to "Failed" and the seat is released
Hard Decline Seat Release and Waitlist Re-Invite
Given a capture attempt returns a hard decline (e.g., insufficient funds, stolen card, do_not_honor) When the decline is received Then the seat is released within 3 seconds and marked available And the next user on the waitlist is auto-invited within 5 seconds And the declined user sees status "Failed" with a prompt to update payment And no charges or partial holds remain on the declined user (any pre-auth is voided) And an event is recorded for creator analytics with reason = hard_decline
Instant Receipts, Ledger Entries, and Wallet/Credits Application
Given a capture succeeds for a seated user When the transaction is finalized Then a receipt is generated and delivered in-app and via email within 30 seconds including currency, tax lines, applied credits, and transaction ID And ledger entries record debit of wallet/credits (if used) and the captured charge with timestamps and amounts And the user history shows the session with status "Captured" and line-item details And the creator dashboard aggregates revenue, tax, and credits used within 10 seconds
Multi-Currency, Tax/VAT, Regional Compliance, and PII Protection
Given a user joins from a region with configured currency and tax rules When pre-authorization or capture occurs Then amounts are displayed and processed in the configured currency with correct minor units And tax/VAT flags determine whether tax is calculated and itemized on receipt and ledger entries And customer payment methods are stored only as provider tokens (no PAN/PII in StreakShare logs or databases) And required compliance metadata (e.g., country code, tax jurisdiction, SCA outcome when applicable) is persisted with the transaction
Early Access Window for Passholders
"As a passholder, I want early door access so that I can secure a seat without last-minute scrambles that could break my streak."
Description

Enable doors to open X minutes early for eligible passholders based on creator-configured rules. Validate eligibility (active pass, role, or membership tier), apply capacity pre-allocation if configured, and send proactive notifications with countdown timers. Ensure timezone/DST correctness and deterministic window computation on both server and client with server as source of truth. If early access fills capacity, route others to waitlist with clear messaging. Log entries and declines for analytics and to detect abuse (multi-device attempts).

Acceptance Criteria
Server-Sourced Early Window Calculation Across Timezones/DST
Given a session with start_at configured in the creator’s timezone Z and early_window_minutes=X And users may be in any device timezone and during DST changes When the client requests door state Then the server returns early_open_at = start_at - X minutes as ISO-8601 with offset and unix timestamp And all clients render a countdown derived from the server timestamp within ±1 second And entry attempts before server early_open_at are rejected with HTTP 403 reason=EarlyWindowClosed And entry attempts at or after server early_open_at are accepted if capacity and eligibility allow
Eligibility Check for Passholders by Pass, Role, or Tier
Given creator rules specifying eligible passes, roles, and membership tiers And a user attempts early entry When the server evaluates eligibility Then users with an active, unexpired, unpaused pass OR qualifying role OR qualifying membership tier are eligible And users with expired, paused, or revoked passes or without qualifying role/tier are ineligible And the API returns eligibility=true|false with reason_code in {ActivePass, RoleAllowed, TierAllowed, PassExpired, PassPaused, PassRevoked, NotInRole, NotInTier} And ineligible early entry attempts receive HTTP 403 with the same reason_code; eligible attempts proceed to capacity checks
Early Access Capacity Pre-Allocation and Waitlist Routing
Given total_capacity=C and early_access_allocation=E where 0 ≤ E ≤ C When early access opens Then up to E eligible users are seated early; the (E+1)th and beyond are not seated And users beyond E are placed on the waitlist with position N and see message "Early access full — you are #N on the waitlist" And the server persists seat assignments, waitlist positions, and transitions with timestamps for audit And users moved from waitlist to seated receive a clear in-app prompt to enter when a seat becomes available before session start
Proactive Countdown Notifications for Early Access
Given early_window_minutes=X and notification_lead_minutes=N where 0 < N ≤ X And a passholder has notifications enabled When the server determines early_open_at Then a single push notification and in-app banner are scheduled at (early_open_at - N) with a live countdown to early_open_at And duplicate notifications for the same session/user are deduplicated within a 24-hour window And if push is disabled or fails, the in-app banner still appears when the app is foregrounded with the correct countdown And delivery and user interaction are logged with outcome in {Delivered, SuppressedDuplicate, PushDisabled, Failed, Tapped}
Client Door States and Countdown from Server Truth
Given the server publishes door_state in {closed, early_open, early_full, open, waitlist, ended} and early_open_at When the client receives an update or polls Then the client displays "Early access" state with countdown to early_open_at when door_state=closed and now<early_open_at And displays "Early access open" with remaining seats when door_state=early_open And displays "Early access full" with waitlist call-to-action when door_state=early_full And reflects server state changes in the UI within ≤5 seconds of the server update
Multi-Device Entry Attempts and Abuse Detection
Given a user initiates early entry from multiple devices within a short interval When the first device is seated Then subsequent concurrent attempts are rejected with HTTP 409 reason=AlreadySeated And all entry attempts (accepted and rejected) are logged with user_id, session_id, device_id, ip_hash, timestamp, outcome, reason And if >3 rejected concurrent attempts occur within 60 seconds, abuse_flag=true is set for the user-session for review
Deterministic Behavior Under Clock Skew and Network Latency
Given a client device clock skew of up to ±5 minutes and variable network latency When the client renders countdowns and attempts early entry Then countdowns are based on server-provided unix timestamps, not local device time And entry attempts are authorized solely by server time; attempts before server early_open_at are rejected with HTTP 403 reason=EarlyWindowClosed And the client resynchronizes server time at least every 30 seconds during the 2 minutes preceding early_open_at
No-Show Auto-Credit Issuance
"As a user who missed a session, I want an automatic credit applied so that I can try again without losing value."
Description

Automatically issue a future-session credit when a user with an eligible reservation fails to check in to seated within the grace period or is unable to be seated due to capacity/technical issues. Define clear eligibility rules (product type, creator policy, time thresholds) and credit properties (expiration, transferability, limits). Credit should appear instantly in the user’s wallet and be auto-applied on the next eligible session. Prevent duplicates with idempotent issuance and include creator overrides for manual grant/revoke. Provide transparent user messaging and audit trails for support.

Acceptance Criteria
No-Show After Grace Period Triggers Auto-Credit
Given a user holds an eligible reservation for a session with creator policy "No‑show credit: enabled" and a defined grace period G minutes And the user does not reach status "Seated" before the grace period ends When the grace period elapses Then exactly one future-session credit is issued for that creator and user And the credit appears in the user's wallet within 5 seconds with status "Active" And no charge is captured for the no‑show reservation And an in‑app and push message is delivered within 30 seconds stating the reason "No‑show" and the credit's expiration date And an audit record is written with reservationId, userId, creatorId, reason "NO_SHOW", and timestamp
Capacity or Technical Failure to Seat Triggers Auto-Credit
Given a user attempts to join an eligible session within the join window And the system fails to seat the user due to "Capacity Full" or a platform "System Error" before the window closes When the failure is detected Then exactly one future-session credit is issued for that creator and user And the credit appears in the user's wallet within 5 seconds with status "Active" And no charge is captured for the failed seating attempt And an in‑app and push message is delivered within 30 seconds stating the specific reason ("Capacity full" or "Technical issue") and the credit's expiration date And an audit record is written with reservationId, userId, creatorId, reason ("CAPACITY" or "TECHNICAL"), and timestamp
Eligibility Gate: No Credit for Ineligible Reservations
Given a reservation is ineligible because the product type is excluded, the creator policy disables no‑show credits, the user reached "Seated", or the user canceled before the start time When eligibility evaluation runs at grace‑period end or on failure Then no credit is issued and the user's wallet remains unchanged And, if messaging is shown, the user sees a clear explanation of ineligibility referencing the creator policy And an audit record is written with reservationId, userId, creatorId, reason "INELIGIBLE", and timestamp
Idempotent Issuance: Prevent Duplicate Credits
Given the same reservation triggers the credit workflow multiple times (scheduler retries, webhook retries, or concurrent attempts) When credit issuance is attempted again Then the system uses an idempotency key derived from reservationId+userId+reason And returns the existing creditId without creating a new credit And the user's wallet contains exactly one credit related to that reservation And audit logs contain one "CREATE" entry and subsequent "SKIP_DUPLICATE" entries
Credit Properties: Expiration, Scope, and Limits Enforcement
Given a credit is issued for a no‑show or seat failure Then the credit expires 30 days after issuance at 23:59:59 UTC on the expiration date And it is non‑transferable and scoped to sessions for the same creator And a user may hold at most 2 active no‑show credits per creator; if the limit is reached, no new credit is issued and the user is messaged about the limit And the wallet displays that the credit covers one future session seat for the creator and shows the expiration date
Auto-Apply Credit on Next Eligible Session
Given the user has one or more active credits scoped to a creator And the user books or is auto‑seated into the next eligible session for that creator before any credit expires When checkout or seating confirmation occurs Then the oldest active credit is auto‑applied, covering one seat and reducing the user charge for that seat to $0 And the wallet updates within 5 seconds to mark that credit as "Applied" with a reference to the new reservationId And only one credit is applied per session; remaining credits persist And if no eligible session occurs before expiry, the credit expires and is not applied
Creator Manual Grant/Revoke with Audit and User Messaging
Given a creator or support agent with "Manage Credits" permission opens the credit management panel for a user When they grant a credit or revoke an existing credit Then the user's wallet updates within 5 seconds reflecting the change And an immutable audit entry is recorded with actorId, action (GRANT or REVOKE), reservationId (if applicable), reason, previousState, newState, and timestamp And the user receives an in‑app and push message within 30 seconds explaining the change and its impact
Window Timeline Door State Indicators
"As a user, I want clear door states on the timeline so that I know exactly when to join or wait."
Description

Expose real-time door states on the Window Timeline with canonical states (Closed, Early Access, Open, Full, Inviting, Cooling Down). Include countdowns to state transitions, capacity badges, and accessibility-compliant labels and colors. Update states via low-latency subscriptions with offline-safe fallbacks and debounced refresh to avoid flicker. Surface actionable CTAs (Join, Join Waitlist, Accept Invite) that reflect eligibility and payment status. Localize labels and ensure consistent behavior across web, iOS, and Android.

Acceptance Criteria
Real-Time Door State Updates with Debounce
- Given a client subscribed to room door-state updates, when the server emits a state change (Closed, Early Access, Open, Full, Inviting, Cooling Down), then the Window Timeline reflects the new state within 1.5 seconds at P95 and 3.0 seconds at P99. - When multiple state changes occur within 300 ms, then the UI performs at most one visible refresh (debounce window = 300 ms) and shows only the final state. - The transition between states is atomic; no intermediate labels are visible for more than a single frame and no flicker is observed in 60 fps screen capture. - If the subscription disconnects, then the client automatically switches to polling every 10 seconds, displays a "Last updated <n>s ago" freshness indicator, and marks data as stale if age > 15 seconds. - Upon reconnection, the client resumes subscription, removes the stale indicator, and reconciles to the latest server state within 2 seconds. - For two clients observing the same room over healthy networks, their displayed state does not differ for more than 2 seconds.
Accurate Countdown to Door State Transitions
- Given a door state with a scheduled transition timestamp (e.g., Closed -> Early Access, Early Access -> Open, Open -> Cooling Down), when viewing the Window Timeline, then a countdown is displayed in mm:ss format that matches server time within +/- 1 second. - When the countdown reaches zero, then the door state changes automatically without user refresh, and the countdown does not display negative values. - When the app returns from background after ≤ 2 minutes, then the countdown and state resync within 1 second; after > 2 minutes, the client performs a refresh and resumes accurate countdown. - When device time is skewed, then server time is authoritative and the countdown remains accurate within the stated tolerance.
Capacity Badge, Full, and Inviting Waitlist Behavior
- The capacity badge displays current/total seats using localized number formatting; when current >= total, the badge shows "Full" and the door state is "Full". - When a seat frees, the capacity current decreases within 1.5 seconds; if a waitlist exists, the door state becomes "Inviting" within 1.5 seconds. - When an invite is accepted, capacity current increases within 1.5 seconds and the state updates per server signal (Inviting/Open/Full) without requiring manual refresh. - Invited users see an "Accept Invite" CTA on the Window Timeline until the server-provided expiry; non-invited waitlisted users see a non-actionable waitlist indicator. - No user sees a "Join" CTA while the state is "Full" unless the server signals they have an active invite.
Contextual CTAs Reflect Eligibility and Payment Status
- Closed: No primary CTA is displayed. - Early Access: "Join" is displayed only to eligible passholders/creator; others see a disabled "Locked" variant with guidance to obtain access. - Open: Eligible users see "Join"; ineligible users see a disabled state with an accessible reason (e.g., age/region/role restriction). - Full: Non-waitlisted users see "Join Waitlist"; waitlisted users see their waitlist status; no conflicting primary CTAs are shown. - Inviting: Invited users see "Accept Invite" until server-provided expiry; others continue to see their prior waitlist or ineligible state. - Cooling Down: "Join" is not displayed; an "Ends in" countdown is shown instead. - If the session requires payment and the user lacks a valid payment method, the primary CTA renders an "Add Payment" variant; attempting to proceed opens the payment flow and does not capture funds at this step. - Initiating any CTA from the Window Timeline does not capture payment until the server confirms the user is seated; no-shows do not trigger capture and are flagged for credit per server rules. - Only one primary CTA is visible at a time; disabled CTAs include an accessible reason string.
Accessible Door States and CTAs (WCAG AA)
- Every door state and capacity badge exposes an accessible name that includes the canonical state and occupancy (e.g., "Door Open, 12 of 20 seats") via platform accessibility APIs. - Color is not the sole indicator of state; each state includes iconography and/or text that is distinguishable without color perception. - Text meets WCAG 2.1 AA contrast (≥ 4.5:1) and essential icons/badges meet ≥ 3:1 in both light and dark mode. - CTAs are reachable via keyboard (Web) and accessibility focus (iOS/Android), with visible focus indicators and logical focus order. - Countdown updates are announced via polite live regions (screen readers) no more than once per second and respect Reduce Motion/Transparency preferences (no flashing/animated tick).
Localized Labels, Numbers, and Time Formats
- Door state labels, capacity terms, and CTAs are localized for all app-supported locales; missing translations fall back to English without placeholder keys. - Numbers and times are formatted per active locale (e.g., digits, thousands separators, 12/24h), and pluralization rules are applied correctly (e.g., seat/seats). - Strings render without clipping or overflow at common phone widths; when truncation is necessary, labels elide gracefully and remain readable. - For right-to-left locales (if supported), layout mirrors appropriately and countdown ordering remains semantically correct.
Cross-Platform Consistency (Web, iOS, Android)
- For the same room and user, Web, iOS, and Android display the same door state and CTA; countdown values differ by no more than +/- 1 second, and capacity values match exactly at the same observation time. - State color tokens and icons follow the same design tokens across platforms; no platform-specific deviations from canonical state-to-style mapping. - Debounce, polling fallback, and stale indicators behave consistently across platforms per the defined thresholds. - Automated visual/snapshot tests verify parity for all canonical states in light and dark modes on each platform.
Creator Door Controls & Overrides
"As a creator, I want simple controls and insights for doors so that I can keep sessions fair, full, and low-admin."
Description

Provide creators with a unified console to configure capacity, early access minutes, pricing, waitlist rules, and grace periods. Include manual overrides to invite/eject, allocate seats, comp or revoke credits, and pause doors during incidents. Offer analytics on fill rate, waitlist conversion, no-show rate, time-to-seat, and revenue captured when seated. Enforce role-based access, log all actions for auditability, and integrate with existing session setup flows to minimize additional admin burden.

Acceptance Criteria
Configure Door Settings in Unified Console
Given I am an authorized creator on a scheduled session When I open the Creator Console and set capacity to 50, early access to 10 minutes, price to $5.00 USD, waitlist rule to FIFO, and grace period to 2 minutes, then click Save Then inputs validate (capacity 1–500; early access 0–60; price >= 0; grace 0–10), settings persist, and the door enforces the new settings within 60 seconds Given any field is out of range or missing When I click Save Then I see inline error messages specifying the violated constraint and the Save action is blocked Given I saved changes When I return to the console Then the last-saved values are pre-populated and an audit record exists with actor, timestamp, and before/after values Given the Creator Console When I navigate from the session card Then I can reach all door settings within 2 clicks and complete configuration in under 30 seconds with defaults pre-filled
Manual Invite and Seat Allocation Override
Given a session is at capacity with a waitlist and I am authorized When I select a waitlisted user and click Invite Now Then the user receives a real-time invite, is moved to Invited state, and their waitlist position is held for 2 minutes Given the invited user accepts within 2 minutes and a seat is available (freed or capacity increased) When they join Then they are transitioned to Seated within 2 seconds and a charge is authorized and captured at seating Given waitlist rule is FIFO When I use Override Order to allocate a seat to a non-first waitlisted user Then that user is seated next, other users keep relative order, and an Order Override audit entry is created Given I cancel the invite or the timer expires When 2 minutes elapse without acceptance Then the user returns to the original waitlist position and the seat is re-offered per rule
Manual Eject and No‑Show Credit Management
Given a user is Seated or In-Room and I am authorized When I click Eject and select a reason (No-show, Disruptive, Technical, Other) Then the user is removed within 2 seconds, the seat is freed, and notifications are sent to both parties Given the reason is No-show and auto-credit is enabled When I confirm Then 1 session credit is added to the user's wallet within 10 seconds and no charge is captured for the ejected session Given the reason is not No-show and I check credit user When I confirm Then 1 comp credit is added and any prior authorization is voided within 15 minutes Given I select do not credit When I confirm Then no credit is issued and captured revenue remains unchanged and all actions are audit logged
Pause and Resume Door During Incident
Given an active session with an Open door When I click Pause Door Then new entries are blocked, auto-invites halt, the door state shows Paused on the Window Timeline within 5 seconds, and no charges are captured during the pause Given a paused door When I click Resume Door Then auto-invites resume per current rules and the door returns to its prior state within 5 seconds Given a paused door with pending invites When I resume Then pending invites older than 2 minutes are re-issued or users are returned to the waitlist per rule, and all actions are audit logged
Role‑Based Access Control for Creator Console
Given roles Owner, Creator, Moderator, Member, and Guest When a user without Creator or Moderator role attempts to open the Creator Console or perform an override Then access is denied (HTTP 403), no data changes occur, and an access-denied event is logged with user id and IP Given a Creator or Moderator When they perform any allowed action Then the action succeeds and the UI shows only permitted controls per role Given role changes in the org/team When roles update Then permissions take effect within 60 seconds across web and mobile
Comprehensive Audit Logging and Export
Given the Creator Console When any configuration change or override is performed Then an immutable audit event is recorded with actor id, role, session id, action type, before/after values, timestamp (UTC), and request origin Given I open the Audit tab When I filter by session, actor, action type, or date range Then results return within 2 seconds for up to 10,000 records and can be exported to CSV Given an unauthorized attempt occurs When it is blocked Then a security audit event is recorded and visible to Creator/Owner only
Door Analytics: Fill, Waitlist Conversion, No‑Show, Time‑to‑Seat, Revenue
Given a session with completed attendance When I open Door Analytics Then the system displays Fill rate (seated_count / capacity at lock time), Waitlist conversion (seated_from_waitlist / total_waitlisted), No-show rate (no_shows / booked), Median time-to-seat (mm:ss), and Revenue captured when seated (sum of charges captured at seating) Given live session activity When seat, eject, invite, or charge events occur Then metrics update within 5 minutes and charts indicate last refresh time Given I click Export When I export analytics Then a CSV downloads with metric definitions in the header and row-level session data for the selected range

Product Ideas

Innovative concepts that could enhance this product's value proposition.

Drift Catcher Ping

Detects near-miss days and sends a timed push with a one-tap check-in deep link to the right room. Saves streaks with a 5-second rescue flow.

Idea

Rolling Window Streaks

Allows flexible 20–28-hour rolling check-in windows tied to each user’s local time shifts. Protects travelers’ streaks without manual edits.

Idea

Ghost Mode Rooms

Lets users join rooms under a pseudonym with masked avatars and fuzzy timestamps. Keeps accountability while shielding identity in public spaces.

Idea

Lockscreen Lightning Check

Adds iOS/Android lockscreen widgets and watch complications for instant one-tap check-ins and quick reactions. Enables discreet tracking during meetings or Do Not Disturb.

Idea

Passkey Pop-In

Uses passkeys for passwordless, two-second sign-in across devices. Reduces drop-off at onboarding and re-entry; FaceID/TouchID unlocks check-ins instantly.

Idea

SmartStart Onboarding

Delivers a 60-second setup that recommends two micro-habits from calendar patterns, then guides the first live check-in. Creates an immediate Day-1 win.

Idea

Creator Pass Rooms

Adds Stripe-powered paid passes and donations for rooms, with supporter badges and timed access. Lets creators monetize accountability sessions without leaving StreakShare.

Idea

Press Coverage

Imagined press coverage for this groundbreaking product concept.

P

StreakShare Launches Rescue Rhythm Suite to Save Streaks Without Disrupting Deep Work

Imagined Press Article

San Francisco, CA — StreakShare, the social habit-tracking app that turns daily routines into live micro-commitment rooms, today introduced the Rescue Rhythm Suite, a comprehensive set of streak-protection features designed for remote knowledge workers and creators who want consistent daily habits without interruptions. The new release brings smarter timing, discreet nudges, rolling-window intelligence, and rapid check-ins together so users can rescue a streak in seconds, even during meetings, travel, or deep work. The Rescue Rhythm Suite centers on three pillars: right-time nudges, flexible windows, and ultra-fast actions. Smart Timing learns a user’s check-in rhythm, calendar gaps, focus modes, and timezone shifts to trigger a rescue ping at the exact moment they are most likely free to act. Early Drift Alert detects midday drift against a user’s typical pattern and sends a friendly heads-up showing time left and the next best check-in, creating a wider runway to act and preventing last-minute stress. When a save is critical, Rescue Ladder provides an opt-in, capped escalation that starts with a gentle push, adds a subtle watch tap, and can fall back to email, Slack, or SMS—stopping the instant a check-in is complete. One-Tap Snooze defers the ping by 5, 10, or 20 minutes with a single tap when a user is in the middle of something, and Stealth Ping delivers a quiet, haptic-first nudge to lockscreen, watch, or widget so streaks can be saved discreetly. To keep streaks aligned with real life, Window Autopilot automatically adjusts rolling check-in windows between 20 and 28 hours based on recent check-in times, sleep/wake patterns, and calendar shifts. Travelers benefit from Jetlag Ramp, which smoothly transitions rolling windows after a timezone change by nudging them 1–3 hours per day, and Auto Timezone Sync, which detects device timezone changes and offers a one-tap align-to-local-time with clear previews and undo. Grace Edge adds a configurable 5–15 minute soft buffer at the end of the rolling window to eliminate near-miss frustration, while Window Timeline visualizes the current window’s start, end, and next reset to give instant clarity on how much time is left. “Streak anxiety is real, especially for remote workers juggling shifting schedules and creators shipping on deadline,” said Ava Lin, co-founder and CEO of StreakShare. “Rescue Rhythm is our promise that your best effort won’t be punished by a calendar surprise or a flight. We built a system that senses drift early, pings at humane moments, and removes friction so you can save a streak in seconds—quietly, confidently, and on your terms.” The suite also introduces Room Flex Sync for shared rooms, applying rolling windows per user while presenting a unified activity band for the group. Hosts can see who’s in-window now to time prompts and reactions, making group sessions fair across time zones. For those who need to act even faster, Raise-to-Check completes a check-in from a watch with a single wrist-raise and tap; Widget Blur hides room names and exact times on the lockscreen while still offering a clear “Check In” button; SafeTap Undo provides a five-second undo after any lockscreen action; Offline Queue preserves streak integrity when users are offline; and Big Tap Mode adapts tap targets to reduce mis-taps during motion or low light. “Our north star is a two-second save,” said Malik Ortega, head of product at StreakShare. “With Pop-In Unlock, Auto-Room Link, and Widget Trust, you’re deep-linked to the exact room and micro-commitment at risk, authenticated via passkey, and able to check in right from the lockscreen or watch. The flow is so fast and discreet that you can protect a streak between back-to-back calls without breaking focus.” Early customers represent a range of StreakShare user types, from the Solo Deep-Worker who values quiet accountability to the Momentum Builder who relies on visible streaks and real-time reactions. “As a distributed teammate, my start time shifts weekly,” said Taylor, a beta user who often travels across time zones. “Window Autopilot and Jetlag Ramp kept my streak alive while my schedule moved. I stopped worrying about missing the window and started trusting the app to meet me where I was.” Rescue Rhythm is designed to work seamlessly with StreakShare’s live micro-commitment rooms. Rooms are social when you want them and silent when you don’t, with quick reactions that deliver motivating feedback without chat noise. The feature set makes it easier for Edge-of-Drift Users to recover momentum, for Room Orchestrators to time prompts, and for Lapsed Returners to re-enter with low pressure and immediate success. Availability and getting started: Rescue Rhythm Suite begins rolling out globally today on iOS, Android, and wearable platforms supported by StreakShare. All users will see Smart Timing, Early Drift Alert, Window Autopilot, and Grace Edge enabled by default with sensible presets, and can personalize their Nudge Blueprint in settings. Raise-to-Check, Widget Blur, and SafeTap Undo are available by adding the lockscreen widget or watch complication. Passkey authentication via One-Tap Enroll, Scan-to-Sign, and Stay Signed is included to keep re-entry instant and secure. For journalists and creators who wish to experience the suite in action, StreakShare offers media demo rooms that simulate tight windows, travel, and calendar shifts. Press and creators can request access via the contact below. About StreakShare: StreakShare transforms daily routines into live micro-commitment rooms for remote knowledge workers and creators ages 20–40. With one-tap check-ins, real-time reactions, and visible streaks, StreakShare boosts adherence, prevents streak decay, and frees hours lost to friction. Media contact: Press Inquiries: press@streakshare.app Partnerships: partners@streakshare.app Website: https://streakshare.app Press Kit: https://streakshare.app/press Forward-looking statements: This press release may contain forward-looking statements about product functionality and availability. Actual results may differ based on platform approvals and regional rollout timing.

P

StreakShare Unveils Ghost Rooms Privacy Suite to Power Anonymous Accountability With Trust

Imagined Press Article

San Francisco, CA — StreakShare today announced the Ghost Rooms Privacy Suite, a set of privacy-first features that deliver the motivating power of social accountability without exposing personal identity. Built for privacy-conscious professionals, creators, and distributed teams, Ghost Rooms combine pseudonymous presence, masked avatars, fuzzy time ranges, and reputation signals to keep rooms welcoming, high-signal, and safe. At the heart of the suite is Pseudonym Picker, which lets users choose a unique alias per room with smart suggestions and collision checks. Avatar Veil replaces personal photos with generative, mask-style avatars that are consistent within a room but distinct across rooms, lightly animated for status such as streak glow. Time Blur allows users to control how precise public timestamps appear—options like “within 15 minutes,” “morning,” or “in-window” maintain accountability while hiding exact routines. “People want to show up for each other without giving up their privacy,” said Lydia Chen, chief trust officer at StreakShare. “Ghost Rooms are a new social contract: you bring consistency and signal; we protect your identity and context. With Pseudonym Picker, Avatar Veil, and Time Blur, you can be seen for your effort, not your metadata.” To sustain high-quality rooms without identity, StreakShare introduces Ghost Cred, a privacy-preserving set of signals including current streak, on-time rate, host-verified sessions, and reaction reliability. Hosts can set minimum Ghost Cred thresholds to join or speak, fostering rooms that stay engaged and supportive. Safe Reveal provides opt-in, time-bound unmasking for edge cases like prize claims, safety checks, or team roll calls. It requires explicit consent, scopes who can see identity, and auto-recloaks after a set window, preserving separation from other rooms and past activity. Moderation and safety are built in. Ghost Guard gives hosts room-level controls to set anonymity levels (full ghost, pseudonym-only, or selective reveal), cap message frequency, rate-limit reactions, and mute or report ghosts without learning their identity. Clear privacy and conduct rules are shown at join so expectations are obvious and misuse is curbed. Shadow Reactions enable motivating feedback through likes, boosts, or emotes that aggregate as counts and transient vibe trails—never tagged to a user—keeping shy participants comfortable while maintaining room energy. “Ghost Rooms are ideal for our Privacy-First Parker persona and anyone who wants accountability without exposure,” said Ava Lin, co-founder and CEO of StreakShare. “But they’re also powerful for Room Orchestrators running public or multi-audience rooms. You can elevate signal and protect safety while still letting momentum and adherence shine through.” Ghost Rooms work seamlessly with StreakShare’s speed stack so privacy never adds friction. Widget Blur hides room names and exact times on the lockscreen while keeping a one-tap Check In button visible. Alias Keys enable separate, pseudonymous passkey profiles per room or audience that switch with a tap and never leak personal identity. Widget Trust opens a short, secure window after one biometric check so lockscreen and watch actions complete instantly while you stay in flow, and SafeTap Undo offers a five-second undo for any accidental tap. For re-entry, Scan-to-Sign and One-Tap Enroll use passkeys for passwordless sign-in across devices; Stay Signed provides a silent, passkey-anchored session refresh; Recovery Circle adds a backup authenticator so you retain control if a device is lost. Early adopters include creator communities that run live publishing sessions, remote teams practicing micro-breaks, and public wellness rooms that wish to welcome newcomers without overexposure. “As someone rebuilding habits, I needed a gentle place to start,” said a Momentum Builder who joined under a pseudonym. “Time Blur gave me space to show up, and Ghost Cred helped me trust the room.” For organizations with compliance or safety needs, Safe Reveal and Ghost Guard provide flexible policies per room. Hosts can require selective reveal for roll calls, keep public rooms fully ghost, or set Ghost Cred minimums during high-traffic times. Everything is transparent at join, including what data is visible and for how long, so participants can make informed choices. Availability and setup: Ghost Rooms Privacy Suite begins rolling out today on iOS, Android, and web. New rooms can select privacy defaults at creation; existing rooms can migrate with a guided helper that previews changes for hosts and members. Pseudonym Picker, Avatar Veil, and Time Blur are enabled by default for public rooms. Ghost Cred thresholds, Safe Reveal rules, and Ghost Guard moderation can be configured in room settings under Privacy & Safety. All features are compatible with Room Flex Sync, ensuring that rolling windows remain fair per user while the group activity band stays unified. “Privacy shouldn’t be a tradeoff against momentum,” added Lydia Chen. “With Ghost Rooms, you can protect your streak and your story at the same time.” About StreakShare: StreakShare turns daily routines into live micro-commitment rooms for remote knowledge workers and creators ages 20–40. One-tap check-ins, real-time reactions, and visible streaks boost adherence, prevent streak decay, and reclaim hours lost to friction—now with privacy by design. Media contact: Press Inquiries: press@streakshare.app Partnerships: partners@streakshare.app Website: https://streakshare.app Press Kit: https://streakshare.app/press Forward-looking statements: This press release may contain forward-looking statements about product functionality and availability. Actual results may differ based on platform approvals and regional rollout timing.

P

StreakShare Introduces Creator Pass Rooms to Monetize Accountability Sessions Without Adding Friction

Imagined Press Article

San Francisco, CA — StreakShare today launched Creator Pass Rooms, a monetization suite that lets hosts earn from live accountability sessions while keeping the check-in experience fast, fair, and privacy-first. With Pass Tiers, Drop-In Pass, Gift Pass, Supporter Afterglow, Support Boosts, and Smart Doors, creators can mix subscriptions and one-time access, reward supporters, and keep rooms energetic without leaving StreakShare. Creator Pass Rooms respond to a simple reality: creators and community organizers shoulder the effort to keep people showing up, but monetizing that value often breaks the flow. StreakShare’s approach integrates payments directly into the same one-tap actions that power check-ins and reactions, so hosts can sustain their work while attendees stay focused on their habits. Pass Tiers offer flexible pricing—Drop-In, Monthly, and Season—with clear perks for each level. Creators can attach benefits such as priority seating, supporter-only prompts, extended grace windows, and distinctive badge styles so fans pick the commitment level that fits. Upgrades and downgrades are one tap, timed access auto-renews or expires cleanly, and supporters never have to leave the app to manage their status. For newcomers or busy schedules, Drop-In Pass provides frictionless, one-time access for a single session or day. Apple Pay and Google Pay complete in seconds, and Auto-Room Link deep-opens to the right room with a preselected micro-commitment for instant check-in. Access is timeboxed to the live session and rolling window, with SafeTap Undo and pro-rated protection if the room schedule shifts. Gift Pass enables supporters, teams, and sponsors to expand a room’s impact. Secure links or QR codes let recipients claim access with Scan-to-Sign passkeys—no passwords—and auto-join under a pseudonym with Time Blur defaults. Bulk gifts and team packs make it simple for Room Orchestrators to sponsor seats for their communities or teammates, all while keeping identities separate and privacy intact. Supporter Afterglow adds an optional, supporter-only cooldown following a session: a focused 3–10 minute micro-room with a single prompt and lightweight reactions. Late joiners receive a two-hour asynchronous window to check in within their rolling window. Badges shine subtly; identities remain pseudonymous. The result is deeper connection without chat noise, meaningful upsell value, and an extra nudge that improves next-day adherence. Support Boosts provide tasteful micro-donations that trigger shadow-style reactions and advance a visible Support Meter toward creator goals, like a bonus session or template drop. Reactions are rate-limited to avoid spam and fully compatible with Ghost Cred and Avatar Veil, so feedback stays motivating and rooms remain welcoming. Smart Doors automate access while keeping the experience fair. Doors open early for passholders, enforce capacity, and manage a waitlist with auto-invites when seats free up. Charges capture only when seated; no-shows automatically credit a future session. Clear door states appear on the Window Timeline so attendees know exactly when to pop in without risking a streak. “Creators shouldn’t have to choose between momentum and monetization,” said Ava Lin, co-founder and CEO of StreakShare. “Creator Pass Rooms weave payments into the same fast, privacy-first flows our users love. Whether someone drops in for a day or commits for a season, the check-in is still one tap and the room energy stays high.” “For me, predictable revenue means I can host more consistently,” said Cam, a creator who runs daily publishing rooms. “Drop-In Pass is perfect for newcomers; Monthly keeps regulars engaged; and Supporter Afterglow gives us a quiet moment to lock in tomorrow’s plan. It all happens without breaking the streak flow.” Under the hood, StreakShare’s passkey stack keeps enrollment and re-entry instant and secure. One-Tap Enroll creates a passkey during onboarding or the next sign-in; Stay Signed provides a silent, passkey-anchored session refresh so Auto-Room Links, lockscreen check-ins, and watch actions just work; Pop-In Unlock ensures users are deep-linked to the right room with FaceID or TouchID in a blink. Alias Keys let creators maintain separate pseudonymous profiles for different audiences while enjoying the same instant sign-in, and Recovery Circle adds a trusted device as a backup authenticator to regain access without passwords or support tickets. Creator Pass Rooms are designed for a range of StreakShare users: Room Orchestrators who host teams or community sprints; Momentum Builders with early streaks who thrive on recurring rooms; Social Accountability Seekers who value visible commitment; and Lapsed Returners who want low-pressure re-entry. The suite elevates the room experience without compromising privacy or speed. Availability and pricing: Creator Pass Rooms begin rolling out today. Pass Tiers, Support Boosts, and Smart Doors are available to all verified hosts. Drop-In Pass and Gift Pass support Apple Pay and Google Pay in supported regions at launch, with additional payment methods planned for future updates. Supporter Afterglow is optional and can be toggled per room with presets for duration, prompts, and badge styles. Hosts can apply via the Creator Console inside StreakShare to enable monetization features and access best-practice templates. About StreakShare: StreakShare turns daily routines into live micro-commitment rooms for remote knowledge workers and creators ages 20–40. One-tap check-ins, real-time reactions, and visible streaks boost adherence, prevent streak decay, and reclaim hours lost to friction—now with built-in tools for creators to thrive. Media contact: Press Inquiries: press@streakshare.app Creator Partnerships: creators@streakshare.app Website: https://streakshare.app Press Kit: https://streakshare.app/press Forward-looking statements: This press release may contain forward-looking statements about product functionality and availability. Actual results may differ based on platform approvals and regional rollout timing.

Want More Amazing Product Ideas?

Subscribe to receive a fresh, AI-generated product idea in your inbox every day. It's completely free, and you might just discover your next big thing!

Product team collaborating

Transform ideas into products

Full.CX effortlessly brings product visions to life.

This product was entirely generated using our AI and advanced algorithms. When you upgrade, you'll gain access to detailed product requirements, user personas, and feature specifications just like what you see below.