Russian version · English is the canonical implementation reference.
Start here. This is the single onboarding document: what VoltFlow is, how data flows from the car to the screen, the rules that keep that data correct, and a map to every other doc. Read this first, then dive into the domain doc for the area you are touching.
Companion repo: VoltFlow Mate (scroodge/BYDMate-own) — the Android gateway app
that runs on the car's DiLink head unit and pushes telemetry to this backend.
A mobile-first PWA for BYD EV owners (built around the BYD YUAN UP / Dolphin platform). It does four things:
- Charging cockpit — live charging sessions with SOC, delivered kWh, tariff-aware cost, ETA, and history. Works even without live car data, by recomputing state from wall-clock math anchored to timestamps in Postgres.
- Vehicle telemetry — ingests state-aware live data from VoltFlow Mate, stores it, and renders live status, trips, GPS tracks, and rich analytics.
- Knowledge base — a Telegram-style CMS (guides, FAQ, accessories, spare parts) with semantic search.
- Service logbook — maintenance records and reminders per car.
Auth, multi-tenancy, and realtime are all Supabase. Every user-scoped table is
protected by Row Level Security keyed on auth.uid().
Di+ / autoservice / GPS on the DiLink head unit
│ local state-aware collection
▼
┌───────────────────────────────────────────────────────────────────────────────────┐
│ VoltFlow Mate APK │
│ Room delivery queue • payload tiering • GPS opt-out • application-ACK retry │
│ driving 1 s | charge <98% 10 s | charge tail 1 s | parked 30 s │
│ flush: driving/tail 15 s, charge bulk 60 s, parked default 60 s │
└──────────────────────────────────────┬────────────────────────────────────────────┘
│ HTTPS batches, X-API-Key + X-Vehicle-Id
▼
┌─────────────────────────────────────┐
│ POST /api/bydmate/telemetry │
│ authenticate → normalize/sanitize → │
│ idempotent ingest → fan-out │
└──────────────────┬──────────────────┘
│
┌───────────────────────────────┼────────────────────────────────┐
▼ ▼ ▼
bydmate_live_snapshots telemetry samples + hourly rollups trips + GPS tracks
(one latest row/vehicle, (tiered raw retention, long-range (server inference)
Realtime source) compact read model) │
│ │ │
└──────────────┬────────────────┴───────────────┬────────────────┘
▼ ▼
VoltFlow PWA / authenticated APIs Telegram live widget
live cockpit • charging • history (server-generated, 30 s throttle)
analytics • trips • export
Separate car-off path: shell-uid CommandDaemon polls/acks commands and sends a reduced
Di+ heartbeat every 60 s only when the app-alive beacon is stale. It never sends GPS and
does not run in parallel with the app's telemetry sender. When a visible VoltFlow view grants
the short-lived fast-status window, both command clients can instead send a `live_only` status
snapshot at the fast cadence; this changes live freshness only, never GPS or durable history.
Separate no-ADB path: cars exposing `energydata` can upload completed trip summaries to
`/api/bydmate/trip-summaries`. They provide trip/consumption history only, not live state,
charging, commands, or tracks.
Two independent live channels reach the PWA:
- Vehicle telemetry is pushed by the car and arrives in the browser via Supabase
Realtime on
bydmate_live_snapshots(not polling). - Charging session progress (
current_percent, energy, cost) is written by the PWA itself (ChargingSessionBackgroundSync) ~1 Hz while charging, and shared across open tabs via Supabase Realtime oncharging_sessions.
The car/Mate ingest never streams per-second SOC into
charging_sessions. It only creates/closes session rows and sets stop-time fields. Per-second progress requires the PWA to be open. This split is the single most common source of confusion — see §4.
The Telegram Mini App (/telegram) is currently the public knowledge base. It does not
read the DiLink head unit or render private live telemetry. The separate Telegram live
widget is generated server-side after an accepted telemetry sample, so it remains useful
when the PWA is closed.
Telemetry health is a separate, event-style Telegram surface: the database's
low_24h_count cadence detector may notify the linked vehicle owner that telemetry has
stopped arriving. It is not a live-widget edit and never sends diagnostic moving-gap
events to owners. Its current behavior and release boundary are documented in
Telemetry.
Normal delivery is deliberately batched to control backend work. While a user has a live
VoltFlow view visible, the PWA renews an expiring, per-vehicle app-owned fast-status window
in Postgres directly through its authenticated Supabase browser client under profile-own RLS;
this does not invoke a Vercel server action. That includes an authenticated Telegram user after
the Mini App opens the dashboard, while the public /telegram knowledge-base route has no
live-view heartbeat. Mate learns the remaining window from its existing command poll and sends a
live_only snapshot on its configured roughly three-second cadence after it observes that
window. Those snapshots update only bydmate_live_snapshots, so they do not create history,
hourly-rollup, or trip rows. The configured delivery behavior is detailed in
Telemetry; it is not an end-to-end latency guarantee.
The window is extend-only and expires automatically; there is no explicit off switch. A closed tab, lost connection, or crashed client therefore returns the vehicle to normal delivery on its next command poll. It is not a user preference and is safe to lose. The existing Realtime subscriber coalesces snapshot changes for one second; observed browser freshness also depends on when the command client learns the grant and on network delivery.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 App Router + React 19 |
| UI | Tailwind CSS 4, shadcn-style components, lucide-react |
| State & data | TanStack Query, Zustand |
| Forms & validation | React Hook Form, Zod |
| Backend | Supabase: Auth, Postgres, Realtime, RLS, Storage, scheduled jobs |
| PWA | manifest.ts, production-only service worker, web push (VAPID) |
| Content discovery | Structured navigation and optional content search |
| Deploy | Vercel (app) + self-hosted Supabase (infra specifics in local docs/OPS_LOCAL.md) |
Routes live under src/app/, grouped by auth layout: (app)/ authenticated,
(auth)/ login/reset, (marketing)/ public. APIs under src/app/api/.
These rules are why the system stays correct across refreshes, reconnects, and PWA
restores. Breaking one of them causes the classic bugs (frozen percent, false
completed, phantom sessions). Full detail in
CHARGING_SESSIONS.md.
- Priority for charging SOC/energy/cost:
fresh live SOC (≤90 s) > in-session telemetry > wall-clock math. Math is a display/persist fallback only — never overwrite fresh live data with math. - Never math-complete a session while fresh live SOC exists. Completion must wait for live SOC so the 100% cell-voltage tail is captured.
- The PWA is the only per-second writer of
charging_sessions. If the row looks frozen butbydmate_telemetry_samplesis current, the user closed the PWA — not an ingest bug. - Charging energy/cost =
SOC_delta% × battery_capacity_kwh ÷ 100 ÷ efficiency. Capacity is per-car/per-user, never hardcoded; efficiency is per tariff (typically ~98% AC and ~90% fast DC), not per car. The BMS counterkwh_chargedmeasures battery-cell energy only (~47 % low vs the grid because of thermal management draw) and must not drive cost — keep it for diagnostics only. See CHARGING_SESSIONS.md §Charging energy & cost. - Auto-detect charging from
charge_power_kw, never tractionpower_kw(the 2026-06-03 phantom-session root cause). vehicle_idis a soft text key, matchedcars.vehicle_alias→ telemetry/tripvehicle_idby string equality. There is no database foreign key yet. History/session APIs must resolve the alias from the car/session — never use a fixed vehicle value in production paths.- RLS scopes every user table by
auth.uid(). The service role key is server-only. - Ownership is explicit. Telemetry, tracks, trip/charge facts, command state and server rollups are user-owned data in Postgres. Mate's Room queue, imported local history and daemon files are device-local delivery/operational caches, never the only source of a user's cloud history. User preferences keep their existing client-side ownership unless a feature explicitly chooses otherwise.
| Subsystem | What it does | Canonical doc | Key code |
|---|---|---|---|
| Telemetry ingest | Validate + persist car data; trigger sessions, widgets and notifications; accept retries/offline batches | supabase/TELEMETRY.md, VoltFlow Mate API | src/app/api/bydmate/telemetry/route.ts, src/lib/voltflowmate/* |
| Charging sessions | Start/stop, ~1 Hz progress, auto sessions, reconcile, tariffs | CHARGING_SESSIONS.md | src/features/charging/ (public entry points plus private domain/server/client/UI implementation) |
| Trips | Server-side trip inference, junk filtering, distance deltas, GPS tracks | TRIPS.md | bydmate_ingest_telemetry (SQL), src/lib/voltflowmate/trip-* |
| Analytics & charts | History→Analytics, trip charts, route maps, route insights | TRIPS.md | src/components/vehicle/*, src/lib/voltflowmate/telemetry-* |
| Notifications | Charge-threshold web push, Android live-status web push, Telegram live-status widget, and linked-owner telemetry-offline notice | VEHICLE_STATE_NOTIFICATIONS.md | src/lib/push/*, src/lib/telegram/* |
| Telegram bot | Mini App login/link (Next.js) + group-message capture, LLM classification, and marketplace draft creation (separate Python edge server, not this Next.js app) | DATABASE_SCHEMA.md §Community marketplace | src/app/api/telegram/{auth,link}/route.ts; scripts/telegram-miniapp-server.py owns the real registered webhook — src/app/api/telegram/webhook/route.ts only sends the PWA deep-link reply and is not the live group-event path |
| Remote commands | Abstract commands PWA → Mate poller or car-off shell daemon (lock, set SOC limit, …) | VoltFlow Mate API | src/app/api/bydmate/commands/*, vehicle_commands |
| Access, donations & retention | Independent account entitlements, donation statistics, tiered telemetry retention, admin tools | PREMIUM_ADMIN.md | is_user_premium(), purge_old_bydmate_telemetry_by_tier() |
| Knowledge base | Content CMS, search, and service catalog | README §Features | src/app/telegram/* |
| Database | Tables, RLS, RPCs, enums, storage buckets | DATABASE_SCHEMA.md | supabase/migrations/ |
| PWA / shell | Install, service worker, mobile nav, i18n (en/be/ru) | INSTALL.md, README §PWA | src/app/manifest.ts, src/components/layout/* |
Current-state review: 2026-08-06, against the checked-in source tree and migration
chain through 20260729190612. This date proves repository reconciliation only; it is not
production deployment evidence.
| Doc | Scope |
|---|---|
| ARCHITECTURE.md | This file — overview, data flow, invariants, doc map |
| ARCHITECTURE.ru.md | Full Russian architecture reference; English remains canonical for implementation |
| ../README.md | Product surface, setup, scripts, project structure |
| ../INSTALL.md | User-facing PWA install guide (RU) |
| CHARGING_SESSIONS.md | Charging sync, auto sessions, reconcile, tariffs, energy/cost |
| TRIPS.md | Trip lifecycle, junk filtering, distance deltas |
| WEB_INTERFACE_FORMULAS.md | Page-by-page map of calculated values shown in the web interface |
| WEB_INTERFACE_FORMULAS.ru.md | Russian translation of the formula reference; English remains canonical |
| KNOWLEDGE_SEARCH.md | Semantic-search indexing, confidence/refusal behavior, evaluation |
| VEHICLE_STATE_NOTIFICATIONS.md | Telegram live-status widget (single message, edited in place) |
| PREMIUM_ADMIN.md | Account entitlements, voluntary donations, and retention tiers |
| DATABASE_SCHEMA.md | Full schema, RLS, RPCs, enums, storage |
| ../supabase/TELEMETRY.md | Telemetry storage model, retention, Di+ fields, analytics APIs |
| VoltFlow Mate API | Mate ingest + command contract |
| Doc | Scope |
|---|---|
| PRODUCT_STATUS.md | Plain-language capabilities + improvement roadmap |
| ../BACKLOG.md | Proposed, unbuilt work and unresolved decisions |
| ../CHANGELOG.md | Shipped outcomes and verification evidence |
| ../supabase/MIGRATIONS_AUDIT.md | Active migration-chain audit and apply constraints |
Historical implementation plans and product research are evidence, not current behavior.
Private endpoints, credentials, account details, and operational runbooks belong only in
ignored local documentation such as OPS_LOCAL.md.
- This is Next.js 16 — treat it as version-specific. Read the relevant guide in
node_modules/next/dist/docs/before changing routing, server actions, metadata, middleware, caching, or file conventions. - Migrations are append-only and idempotent. Never edit an applied migration; add a new, guarded migration instead.
- Tests use Node's built-in runner with
--experimental-strip-types(no Jest/Vitest). Tested pure modules must use relative.tsimports, not@/aliases.npm run testrecursively discovers every.test.mjsfile undersrc/, including charging and nested telemetry suites.npm run test -- --listlists files without executing them; discovery does not depend on shell glob expansion. - When behavior changes, update the matching doc (see §6) and add/adjust tests for parser logic, charging completion, trip filtering, telemetry history, or push thresholds.