The data model
Your export is a SQLite file — the real database the app reads and writes, not a cache. (NDJSON / CSV / Parquet exports carry the same fields, flatter.) Every table below is yours to query directly.
From sensor to your own tools
Your phone and Chrome each keep a local database. Record with a sensor over Bluetooth Low Energy (BLE), then manually move records between devices using a link, QR code, file, or temporary cloud share.
Eject DB saves a .tensr.db file you can inspect with a SQLite viewer on your computer, or give to Claude or another agent with file access and SQLite tools. A temporary cloud share is a short-lived hand-off; it does not enable automatic background sync or create a lasting cloud backup. See planned cloud services.
How the main tables connect
Start with session: one workout. Its child tables hold the movement summaries, planned-movement outcomes, raw force recording, and ambient channels. A saved routine has its own version history; a session can reference the routine it followed.
One session can have many segments, planned movements, and channels, and at most one force recording. A skipped planned movement can have asession_movement row without a recorded segment.movement_kind and movement_key identify either a canonical catalog movement or a custom_movement; this lookup is not an enforced foreign key. Sessions, routines, goals, and custom movements also belong to a workspace, omitted from the diagram.
Inspect your data
Use the app's Data tab to run SQL against the database on that device. To browse an exported.tensr.db file, open it inSQLite Viewer or another SQLite viewer. SQL-capable tools can also run the queries below against your export.
Schemas evolve. Discover before you assume — start fromsqlite_master, then query what actually exists in your export.-- What tables exist? (skip the sync-internal _ tables)
SELECT name FROM sqlite_master
WHERE type='table' AND name NOT LIKE '\_%' ESCAPE '\';
-- What columns does a table have?
SELECT sql FROM sqlite_master WHERE type='table' AND name='session_segment';Core tables
| Table | One row is… | Key columns |
|---|---|---|
session | one workout | started_at, ended_at, duration_sec, total_impulse_kgs, total_volume_kg, total_rep_count, routine_title |
session_segment | one movement within a session | movement_key, impulse_kgs, rep_count, volume_kg, max_rep_peak_kg, side, started_at/ended_at |
session_movement | one planned movement and its outcome | movement_index, movement_key, status (pending, recorded, skipped, or not reached) |
session_channel | an ambient channel (e.g. heart rate) | channel_kind ('hr'), stats_json (bpm min/avg/max + recovery) |
session_recording | the raw force curve | encoded — an encoded blob, not plain SQL; the per-segment summaries are computed from it |
canonical_movement / canonical_alias | the open movement catalog | slug, name, muscles. Join session_segment.movement_key → slug for names |
custom_movement | a movement you named yourself | id, name, aliases, primary_muscles |
goal | an active goal | metric, dimension, target, window |
routine / routine_version | a saved program (versioned) | title, ast_json (the routine tree) |
Tables prefixed _ (e.g. _changes, _sync_state) are sync/internal — ignore them for analysis.
The metric vocabulary
tensr records force over time, so its numbers say more than sets × reps:
- Impulse (kg·s) — force integrated over time; the headline volume metric (
impulse_kgs). - Peak force (kg) — the hardest instant of a set (
max_rep_peak_kg); the intensity proxy. - Per-rep metrics — require a decoded recording. Set peak, rep count, and impulse alone do not reveal an effective-rep count or proximity to failure.
- RFD — rate of force development — how fast you produce force ("how fast you get tight"); a strength/explosiveness lever.
- Time under tension and tempo — need suitable decoded samples; segment duration alone does not establish loaded time or eccentric speed.
- HR statistics — inspect available
session_channel.stats_jsonkeys before using average, peak, or recovery values. Missing records do not establish whether a strap was worn.
Starter query
Open this query in the Data tab →to inspect the records stored on your current device. The link carries the SQL; it does not transfer training data between devices.
-- Per-movement history, newest first
-- Custom movement names when available; catalog movements use their keys.
SELECT date(s.started_at) AS day,
COALESCE(cm.name, seg.movement_key, 'Unassigned') AS movement,
seg.rep_count AS reps,
round(seg.impulse_kgs) AS impulse_kgs,
round(seg.max_rep_peak_kg, 1) AS peak_kg
FROM session_segment seg
JOIN session s ON s.id = seg.session_id
LEFT JOIN custom_movement cm
ON seg.movement_kind = 'custom' AND cm.id = seg.movement_key
ORDER BY s.started_at DESC
LIMIT 100;This query works with the app's tables and exported databases. Catalog movements appear by key; custom movements use their saved names. If your export includes canonical_movement, you can also join itsslug to movement_key for catalog names.