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.

Sensors send readings over BLE to a phone or Chrome. Their local SQLite databases exchange records through manual transfers. Eject DB exports a portable file to your own tools.
Each device holds a working copy. Transfers happen when you choose.

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.

A routine has versions and can be referenced by sessions. Sessions have segments, planned movements, an optional force recording, and ambient channels.
Solid lines = foreign keys. Dashed line = logical reference.

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

TableOne row is…Key columns
sessionone workoutstarted_at, ended_at, duration_sec, total_impulse_kgs, total_volume_kg, total_rep_count, routine_title
session_segmentone movement within a sessionmovement_key, impulse_kgs, rep_count, volume_kg, max_rep_peak_kg, side, started_at/ended_at
session_movementone planned movement and its outcomemovement_index, movement_key, status (pending, recorded, skipped, or not reached)
session_channelan ambient channel (e.g. heart rate)channel_kind ('hr'), stats_json (bpm min/avg/max + recovery)
session_recordingthe raw force curveencoded — an encoded blob, not plain SQL; the per-segment summaries are computed from it
canonical_movement / canonical_aliasthe open movement catalogslug, name, muscles. Join session_segment.movement_key → slug for names
custom_movementa movement you named yourselfid, name, aliases, primary_muscles
goalan active goalmetric, dimension, target, window
routine / routine_versiona 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:

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.

→ Hand this model to your agent and let it do the querying