Index

Streakly

Privacy-first habit cessation and behavioral tracking Progressive Web Application built with an offline-first client architecture and zero-backend telemetry.

Creator & Lead Frontend Engineer2025 — PresentPersonalView live ↗
100%Offline data availability
0 KBServer telemetry overhead
467 KiBPrecache bundle footprint

Privacy-Sensitive Recovery & Zero-Backend Architecture

Behavioral recovery and habit cessation tracking involve deeply sensitive personal data. Standard commercial habit trackers routinely sync relapse timestamps, mood check-ins, and raw reflective journals to remote cloud databases, introducing privacy risks, analytics tracking, and credential friction.

In developing mobile regions—such as East Africa where mobile users frequently manage metered data bundles and endure intermittent cellular connections—cloud-dependent habit applications regularly stall during critical moments of acute urge management. A user seeking an grounding session or logging an immediate relapse cannot wait for remote API handshakes.

Streakly was engineered as a zero-backend, offline-first Progressive Web Application (PWA). All persistent state—start timestamps, relapse records, mood evaluations, journal entries, and milestone badges—lives exclusively within browser localStorage. By eliminating remote servers, user authentication, and third-party tracking scripts, the application guarantees absolute user privacy, zero cloud operational expenses, and sub-5ms read/write execution.

Streakly habit cessation interface showing live streak progress and interactive controls
Mobile-first PWA interface featuring real-time streak timer, milestone tracking ring, and zero-telemetry client storage.

Client-Side Interval Mathematics & Schema Migration

Many habit trackers compute streak progression by tracking simple calendar-day transitions. This naive approach introduces false milestones across timezone shifts and fails to reward true 24-hour periods of discipline. Streakly computes streaks strictly against elapsed 24-hour epochs (`Math.floor((now - startedAt) / msPerDay)`), anchoring progress to verified elapsed duration rather than calendar midnight rollovers.

An early iteration of the client persisted relapse history as a simple array of ISO timestamp strings (`string[]`). While sufficient for counting total relapses, this schema failed to preserve interval duration: whenever a user reset their counter, the start date was overwritten immediately, collapsing past streak intervals to zero milliseconds and making accurate all-time longest streak calculations impossible.

Rather than wiping client state during an application update, an idempotent migration routine (`migrateRelapseHistory`) executes synchronously at app boot before component hydration. It safely parses raw localStorage records, validates the payload against corruption, and backfills relational intervals into structured `StreakSession[]` objects (`{ startedAt, endedAt }`). Downstream components compute monthly clean-day distributions and all-time maximum streaks across completed and active sessions with mathematical consistency.

src/utils/storage.ts
export interface StreakSession {
  startedAt: string;
  endedAt: string;
}

/**
 * Converts legacy relapse timestamp arrays to structured StreakSession intervals.
 * Reconstructs startedAt boundaries to preserve historical duration data across updates.
 */
export const migrateRelapseHistory =  (): void => {
  const raw = localStorage.getItem(STORAGE_KEYS.RELAPSE_HISTORY);
  if (!raw) return;

  let parsed: unknown;
  try {
    parsed = JSON.parse(raw);
  } catch {
    localStorage.removeItem(STORAGE_KEYS.RELAPSE_HISTORY);
    return;
  }

  if (!Array.isArray(parsed) || parsed.length === 0)  return;
  if (typeof parsed[0] === 'object' && parsed[0] !== null && 'startedAt' in parsed[0]) return;

  const oldDates = (parsed as string[])
    .map(s => new Date(s).getTime())
    .sort((a, b) => a - b);

  const startDate = localStorage.getItem(STORAGE_KEYS.START_DATE);

  const sessions: StreakSession[] = oldDates.map((endedAt, i) => {
    let startedAt: number;
    if (i === 0) {
      const startTs = startDate ? new Date(startDate).getTime() : 0;
      startedAt = startTs < endedAt ? startTs : endedAt;
    } else {
      startedAt = oldDates[i - 1];
    }
    return {
  startedAt: new Date(startedAt).toISOString(),
      endedAt: new Date(endedAt).toISOString(),
    };
  });

  localStorage.setItem(STORAGE_KEYS.RELAPSE_HISTORY, JSON.stringify(sessions));
};

Client-side schema migration engine. Parses legacy unversioned ISO strings, reconstructs interval boundaries (startedAt to endedAt), and guards against corrupt local storage payloads before component hydration.

467 KiBPrecache asset bundle
100%PWA offline availability
$0Cloud infrastructure cost
[< 350ms]First contentful paint

PWA Lifecycle, Service Worker Caching & Android TWA Readiness

The application integrates `vite-plugin-pwa` with Workbox to configure an aggressive pre-caching strategy (`clientsClaim`, `skipWaiting`, `autoUpdate`). All application assets—including web fonts, UI icons, SVG manifests, and route-level code chunks—are cached in CacheStorage during the initial service worker registration. Once installed, the application functions entirely without network connectivity, surviving high-latency 2G/3G environments and airplane mode without degraded functionality.

In addition to browser installation, the distribution layer contains verified Android Digital Asset Links (`public/.well-known/assetlinks.json`) mapped to package name `app.netlify.nofappr`. This configuration enables packaging directly into an Android Trusted Web Activity (TWA), delivering native APK distribution capabilities without maintaining a separate mobile codebase.

By enforcing strict architectural constraints—zero backend telemetry, client-side data migrations, and deterministic local storage primitives—Streakly demonstrates that high-utility personal recovery software can be delivered with zero ongoing cloud hosting expenses, complete user sovereignty, and predictable sub-second responsiveness.