Skip to content

Navigation Menu

Sign in
Sign up

Repository files navigation

Open Live Tracking

Real-time location sharing. Create a link, share it, let others follow your position on a map — no account required.

How it works

  1. Host opens the app, sets an expiry time (15min to 24h), clicks "Create sharing link"
  2. Host copies the link and shares it (WhatsApp, SMS, etc.)
  3. Viewers open the link and see the host's position updating on a map in real time; a "Show trail" button appears once the first position arrives to toggle the route polyline
  4. Host can stop sharing at any time by clicking "Stop sharing"

Web version: The app requests a Wake Lock to prevent auto-sleep, but manually locking the screen pauses tracking — a fundamental browser limitation. Keep the screen open for uninterrupted sharing.

Android app: True background tracking. The app continues sending position updates even with the screen locked, powered by a foreground service via Capacitor + @capgo/background-geolocation.

Tech stack

  • Frontend: React 18 + TypeScript + Vite + react-leaflet + OpenStreetMap
  • Backend: Supabase (Postgres + Realtime Postgres Changes)
  • Native (Android): Capacitor + @capgo/background-geolocation (foreground service, distance-based updates)
  • Testing: Vitest (unit/integration, 100% coverage gate) + Playwright (E2E)
  • Deploy: GitHub Pages (via GitHub Actions) / Android APK via Android Studio

Local development setup

1. Prerequisites

  • Node.js >= 22
  • A Supabase project (free tier works)

2. Clone and install

git clone <repo-url>
cd open-live-tracking
npm install

3. Configure environment

cp .env.example .env

Edit .env with your Supabase project credentials (Settings → API):

VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-here

4. Apply the database schema

Run the migration in Supabase → SQL Editor:

# Contents of supabase/migrations/001_create_sessions.sql

Or with the Supabase CLI:

supabase db push

The migration creates the sessions table, RLS policies, enables Realtime Postgres Changes, and adds a cleanup_expired_sessions() function.

Confirm Realtime is enabled: Supabase → Database → Replication → sessions should be listed under the supabase_realtime publication.

5. Start the dev server

npm run dev # HTTP — works for desktop testing
npm run dev:https # HTTPS via mkcert — required for geolocation on other devices

Open http://localhost:5173.

To test from a mobile device on the same Wi-Fi network, use npm run dev:https. On first run, mkcert generates a locally-trusted certificate (may prompt for sudo). Access the app at https://<your-local-ip>:5173.

Running tests

npm run test # Unit + integration tests (fast)
npm run test:coverage # Same + 100% coverage gate (fails if below)
npm run test:e2e # Playwright E2E against a real Supabase project
npm run test:e2e:ui # Playwright with interactive UI
npm run lint # ESLint, zero warnings allowed
npm run build # TypeScript check + production build

The coverage gate covers all statements, branches, functions, and lines. /* v8 ignore */ is only allowed for unreachable defensive guards — never for business logic paths.

Android build

The android/ directory is generated by Capacitor and tracked in git. To build and deploy to a device:

npm run build # build web assets into dist/
npx cap sync android # copy dist/ + sync plugins into android/
npx cap open android # open Android Studio

In Android Studio: Run ▶ to install on a connected device or emulator. For a signed release APK: Build → Generate Signed Bundle / APK.

First-time setup (Ubuntu/Linux via snap):

# 1. Install Android Studio
sudo snap install android-studio --classic
# 2. Download standalone cmdline-tools (find latest version at dl.google.com/android/repository/repository2-3.xml)
mkdir -p ~/Android/Sdk/cmdline-tools
curl -o /tmp/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-<version>_latest.zip
cd ~/Android/Sdk/cmdline-tools && unzip /tmp/cmdline-tools.zip && mv cmdline-tools latest
# 3. Install SDK components
export JAVA_HOME=/snap/android-studio/current/jbr
export ANDROID_HOME=~/Android/Sdk
export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin
yes | sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"

Add to your shell config (~/.config/fish/config.fish or equivalent):

set -x ANDROID_HOME $HOME/Android/Sdk
set -x JAVA_HOME /snap/android-studio/current/jbr
set -x CAPACITOR_ANDROID_STUDIO_PATH /snap/android-studio/current/bin/studio.sh
set PATH $PATH $ANDROID_HOME/platform-tools $ANDROID_HOME/cmdline-tools/latest/bin $JAVA_HOME/bin

Accept any SDK license prompts that appear on first sync.

Permissions granted by the plugin at runtime: ACCESS_FINE_LOCATION (foreground) and ACCESS_BACKGROUND_LOCATION (background). The plugin's requestPermissions: true option prompts the user automatically on first use.

Architecture

Data flow

Host (web browser)
 └─ navigator.geolocation.watchPosition (restarts on visibilitychange, throttle 3s)
 └─ useGeolocation — web path
Host (Android native app)
 └─ BackgroundGeolocation.start (foreground service, distanceFilter 10m)
 └─ useGeolocation — native path (screen locked ✓, Doze mode ✓)
Both paths → useSession.sendPosition(PositionPayload)
 └─ tracking.ts: updatePosition()
 └─ Supabase: UPDATE sessions SET last_lat=..., last_lng=...
Supabase Realtime (Postgres Changes on sessions table)
 └─ useRealtimePosition (viewer hook)
 └─ ViewerPage → trail[] accumulated in state (client-side only, not persisted)
 └─ MapView (marker + optional Polyline when showTrail=true)

Realtime strategy

Position updates go via Postgres Changes (not Broadcast channels). The host writes to the sessions row; the viewer subscribes to UPDATE events on that row. This means:

  • Late-joining viewers always get the current position from the initial SELECT
  • No separate "replay" mechanism needed
  • Only the latest position is stored, no history
  • Viewers accumulate the trail client-side in memory (ViewerPage state); refreshing the page clears it

Session lifecycle (host)

idle → restoring → active → stopped
 → creating ↗ ↘ error

restoring: on page reload, the app attempts to recover an active session from sessionStorage (key: olt_session_token). This handles the case where the browser background-killed the tab while sharing was active.

Session lifecycle (viewer)

loading → invalid (no such token)
 → waiting (valid session, no position yet)
 → live (position available, recently updated)
 → stale (no update for >30s)
 → expired (expires_at passed)
 → revoked (active=false, host stopped sharing)

Token security

Each session uses a nanoid(21) token (~126 bits of entropy) as the URL path. The token is both the session identifier and the access control mechanism. Expiry and revocation are validated server-side via Supabase RLS and the active column.

Known limitations: The RLS SELECT policy uses using(true) — any Supabase client with the anon key (which is embedded in the JS bundle and therefore public) can read all active sessions, not just the one whose token they know. The UPDATE policy has the same bearer-token scope: a viewer who knows the URL can technically modify the session row directly via the Supabase API. For a production deployment, route viewer fetches and host mutations through an authenticated Edge Function instead of direct client access.

Swapping the backend

All Supabase calls are isolated in src/lib/tracking.ts. Public interface:

createSession(opts: { expiresInMinutes: number }): Promise<SessionRow>
getSession(token: string): Promise<SessionRow | null>
updatePosition(token: string, position: PositionPayload): Promise<void>
revokeSession(token: string): Promise<void>
isSessionValid(session: SessionRow): boolean

To swap to Firebase, Node+WebSocket, or another backend:

  1. Rewrite src/lib/tracking.ts
  2. Rewrite src/lib/supabase.ts (or remove it)
  3. Rewrite src/hooks/useRealtimePosition.ts for your transport
  4. No changes needed to components or pages

Database schema

create table public.sessions (
 token text primary key,
 created_at timestamptz not null default now(),
 expires_at timestamptz not null,
 active boolean not null default true,
 last_lat numeric,
 last_lng numeric,
 last_accuracy numeric,
 last_updated_at timestamptz
);

See supabase/migrations/001_create_sessions.sql for the full migration (RLS policies, Realtime, cleanup function).

Optional: scheduled cleanup

Schedule cleanup_expired_sessions() to mark expired sessions as inactive:

select public.cleanup_expired_sessions();

Use Supabase's built-in cron (Database → Functions) or pg_cron if enabled.

CI / CD

GitHub Actions runs on every push to main and develop, and on pull requests to main.

The four main jobs run in parallel:

Job What it does
Lint ESLint, zero warnings
Unit Tests Vitest with 100% coverage gate
Build tsc --noEmit + vite build with correct GitHub Pages base path
E2E Tests Playwright against the real Supabase project

Deploy to GitHub Pages runs after all four pass, but only on push to main.

Required secrets

Add these to your GitHub repository (Settings → Secrets and variables → Actions):

Secret Where to find it
VITE_SUPABASE_URL Supabase → Settings → API → Project URL
VITE_SUPABASE_ANON_KEY Supabase → Settings → API → Project API keys → anon

Use a dedicated Supabase test project for CI, separate from production.

Enable GitHub Pages

In your repository settings: Pages → Source → GitHub Actions.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /