CASE STUDY / GYM TRACKER
A chat boundary around a training database
The Gym Tracker is a workout log, analytics dashboard, and AI coach. The interesting part is the boundary between the coach and the database: every query is inspected, constrained, enriched, and returned with enough context for the model to recover when the first attempt is wrong.
958
LINES
AST validation in sql-policy.ts
SELECT
POLICY
Read-only queries reach Postgres
LIVE
CATALOG
Schema and body-part values join the prompt
R3F
VISUALS
3D body map beside Recharts panels
01 / SQL POLICY
The model gets a boundary, not a database connection.
I do not let the model send arbitrary SQL to PostgreSQL. Its query passes through validateAndRewriteSql, which parses the statement with pgsql-ast-parser, rejects forbidden syntax, checks tables and columns against the allowlist, parameterizes string literals, validates CTE scope, and applies a limit and time window.
Only a single SELECT or WITH ... SELECT is accepted. The policy returns rewritten SQL and parameters for the executor. The boundary is explicit in code, so a prompt cannot turn a coaching question into a write operation.
- SQL policy AST validator
02 / PROMPT CONTEXT
The prompt knows the current shape of the data.
The chat route builds its system prompt from the catalog, metric definitions, semantic query hints, and the current body-part values. The catalog reads the allowed tables and columns from information_schema, caches the result, and keeps a fallback schema for local or unavailable database connections.
Body parts are loaded separately from body_parts. The model receives the exact keys it can use in filters and gym_lifts_v comparisons. That removes a common source of failure: inventing a label that is close to the display name but not valid in the data.
- Prompt construction and tool results
- Live schema and body-part catalog
03 / DATA MODEL
The view resolves anatomy before the chat has to reason about it.
gym_lifts stores the exercise text that was logged. The gym_lifts_v view joins that text to exercises and exercise_aliases, then exposes canonical_name and body_part_key. An entry such as RDL can resolve to the canonical exercise without making every model query repeat alias logic.
The chat keeps planned intent separate from logged work. gym_day_meta.body_parts describes the muscles planned for a training day. gym_lifts_v.body_part_key describes the muscles attached to sets that were actually logged. Comparing the arrays makes gaps visible instead of treating a plan as proof of completed work.
- Muscle-aware view migration
- Prompt construction and tool results
04 / FAILURE PATH
A failed query becomes useful context for the next turn.
The executor returns the query id, purpose, rewritten SQL, parameters, row count, preview rows, applied policy, and error. Validation failures are captured in the same shape as database failures. The model can see what failed, explain what is missing, and choose a narrower follow-up instead of receiving a generic server error.
The response also limits preview rows while preserving the total row count. That keeps the conversation small without hiding whether a result is a sample or the full result set.
- Prompt construction and tool results
- SQL policy AST validator
05 / VISUAL LAYER
The dashboard turns the same model into a review surface.
The dashboard uses Recharts for time-series volume and body-part comparisons. React Three Fiber renders the interactive body diagram, so the data can be read as a trend, a table, or a map of trained areas. Each view answers a different question without asking the chat layer to carry the whole interface.
The result is a small system with a clear division of work: PostgreSQL stores the log, the policy controls access, the prompt supplies the vocabulary, the model interprets returned rows, and the dashboard makes the pattern visible.
- Gym dashboard chart composition
- Recharts volume chart
- Three.js body diagram
SOURCE INDEX
Read the implementation
The repository is public. These links point to the files behind the decisions described above.
- SQL policy AST validator
Parsing, allowlist checks, parameterization, limits, and CTE scope.
- Prompt construction and tool results
Live context injection, SQL rules, and enriched query errors.
- Live schema and body-part catalog
Allowed tables, database introspection, caching, and exact body-part keys.
- Muscle-aware view migration
Canonical exercise and alias resolution before the chat query runs.
- Gym dashboard chart composition
Recharts panels and the React Three Fiber body diagram in the dashboard.
- Recharts volume chart
The area chart that turns daily volume into a readable trend.
- Three.js body diagram
The 3D surface that maps training volume onto body parts.