Self-hosted · HA-deployed · Zero-touch

Video calls built for
the people who need it most

A purpose-built two-sided communication system — a locked-down Android kiosk for seniors, paired with a native app and PWA for family. One tap to connect. No tech skills required.

View on GitHub How it works →

System flow

Kiosk Tablet LiveKit WebRTC Family App / PWA
Hono API · SQLite WAL R2 · FCM · Web Push Cloudflare Tunnel · Traefik · Docker Swarm
4
Deployable components
(2 APKs + 2 web apps)
214
Automated E2E tests
passing in CI
OTA
Zero-touch APK updates
over Cloudflare R2
HA
3-node Proxmox cluster
with instant failover
0
Open inbound ports
via Cloudflare Tunnel

Designed for both ends
of the conversation

The kiosk tablet lives with the senior. The family app lives on everyone else's phone. Each is purpose-built for its user.

Kiosk — patient side
  • Hardened Lock Task Mode via Device Policy Manager — user cannot exit
  • Auto wake-on-call — screen turns on and bypasses keyguard for incoming calls
  • One-tap answer — no unlocking, no navigation
  • Remote admin control — volume, brightness, orientation, font scale
  • Bluetooth SCO routing — compatible with hearing aids and specialized headsets
  • OTA auto-update — pulls validated APK releases silently, no user action
  • Daily scheduled restart + last-gasp crash reporting to backend
  • Offline-first photo slideshow via locally-cached R2 assets
WebRTC
Family app — Android APK + PWA
  • QR code or deep-link device pairing
  • One-tap outbound video call with ringback tone
  • Call declined feedback — instant notification when kiosk declines
  • Callback requests — "Thinking of you" prompts on the kiosk
  • Recent call history — last 3 calls with answered/declined/missed status
  • PiP mode and intelligent audio routing (speaker / earpiece / BT SCO)
  • Push notifications — Web Push (PWA) and FCM (Android APK)
  • OTA self-update through same pipeline as kiosk APK

Engineering details
that matter

The hard problems in building for seniors aren't in the happy path — they're in the edge cases.

🔒

Device Owner Kiosk Mode

Provisioned as Android Device Owner via ADB or QR setup. Lock Task Mode enforced by Device Policy Manager — camera and microphone permissions granted automatically, no user prompts.

🛡️

Encrypted Credential Storage

Family APK stores all credentials (device token, API key) in EncryptedSharedPreferences via SecurityUtils. No plaintext secrets on-device. VAPID keys are server-side only.

📡

SDK-less Cloud Integrations

Google OAuth2 and FCM v1 signaling implemented manually using Node's native crypto library — minimal container footprint, no heavy SDK dependency chains.

Synchronous SQLite in WAL Mode

Better-SQLite3 with WAL mode and a single-thread sync model for zero-latency kiosk heartbeat processing. No async database races, deterministic state.

🌐

JS Bridge Hardware Access

KioskJsInterface exposes Android hardware to the React WebView — volume, brightness, wake locks, Bluetooth SCO. The Kotlin layer intercepts WebView requests to serve locally cached R2 photos offline.

🔁

OTA Deployment Pipeline

Both APKs self-update via a lifecycle manager that polls the backend, downloads from R2 (5-min OkHttp timeout), and installs silently through Device Policy Manager — no Play Store required.

🛡️

Timing-Safe Auth

Device tokens verified using constant-time cryptographic comparison to prevent timing side-channel attacks. Admin routes require API key; tablet routes use x-device-id scoping.

🧪

214-Test E2E Pipeline

Full lifecycle coverage — patient and contact creation, photo ordering, call flow, auth enforcement (401 on bad key), and CORS verification. Test data is deterministically isolated from production.

Built for reliability,
not just demos

Zero-downtime deployments on a self-hosted private cloud. No open inbound firewall ports — all traffic flows through a Cloudflare Zero Trust Tunnel.

🏗️

3-Node Proxmox HA Cluster

Hypervisor fabric across three PVE nodes — instant VM failover, split-brain mitigation, and live migration without downtime.

🐳

Docker Swarm · Multi-replica Stack

Backend API runs as a replicated Swarm service. Deploy triggers a rolling update — new container starts before old one stops.

🔀

Traefik v3 + Cloudflare Tunnel

Traefik handles internal routing and TLS termination. Cloudflare Tunnel forwards public traffic — no exposed ports on the host network.

☁️

Cloudflare R2 Asset Distribution

APK binaries and slideshow photos staged in R2. Kiosk tablets cache photos locally via transparent WebView request interception — offline-first.

# docker-swarm-stack.yml (simplified) services: family-api: image: registry/family-kiosk-api:latest deploy: replicas: 2 update_config: order: start-first secrets: - family_api_key - livekit_api_key - r2_access_key_id - vapid_private_key labels: - "traefik.enable=true" - "traefik.http.routers.family-api.rule= Host(`family.YOUR_DOMAIN`)" # Secrets injected at runtime via Docker secrets # (not in env vars, not in image layers)

Lean Hono API
on SQLite WAL

No ORM. No connection pools. Synchronous better-sqlite3 on a single worker — predictable, fast, and simple to reason about.

# Core tables patients — patient/elder records contacts — family members per patient device_storage — tablet heartbeat & health active_rooms — live LiveKit sessions # Device registration uses # INSERT ... ON CONFLICT DO UPDATE # — idempotent, no duplicates # Secrets: never in DB or env vars # Injected via Docker secrets → /run/secrets/ # Backed up in Vaultwarden

Three auth scopes,
clearly separated

Admin routes, tablet routes, and family device routes each have distinct auth mechanisms and access patterns.

🔑

Admin / Webhook routes

Protected by shared x-api-key header. Manages patients, contacts, photos, APK releases.

📱

Tablet (kiosk) routes

Auth via x-device-id. Tablet registers itself on first boot, heartbeats every minute, receives call signals.

👨‍👩‍👧

Family device routes

Paired via QR/deep-link, assigned a device_token. Used for call initiation, status polling, callback requests.

The full picture

Purpose-picked tools — no framework churn, no unnecessary abstractions.

🤖

Kotlin / Android

Kiosk APK + Family APK

⚛️

React 19 + Vite

Kiosk UI + Family PWA

🔥

Hono

Node.js API framework

🗄️

SQLite (WAL mode)

better-sqlite3, sync model

📹

LiveKit

WebRTC video / audio

🔔

FCM + Web Push

Push notifications, both platforms

☁️

Cloudflare R2 + Tunnel

Asset CDN + zero-port ingress

🐳

Docker Swarm

Multi-replica HA deployment

🔀

Traefik v3

Reverse proxy + TLS

🖥️

Proxmox VE

3-node HA hypervisor cluster

🔐

Vaultwarden

Self-hosted secrets backup

🧪

PowerShell E2E

214 tests, full lifecycle coverage

Self-host in three steps

The backend deploys as a Docker stack. The kiosk APK provisions itself as Device Owner. Family members pair with a QR code.

1

Deploy the backend

Configure stack/family-kiosk.yml with your domain and Docker secrets. Run deploy-backend.ps1 to build and push to your Swarm cluster.

2

Provision the kiosk tablet

Factory reset an Android tablet, tap the welcome screen 6× to enter QR provisioning mode, scan the generated code. The APK installs and sets itself as Device Owner automatically.

3

Pair family members

Admin generates a pairing QR code or deep link. Family member scans it in the Family App or PWA — device token is issued, push subscriptions registered, done.