{
 "id": "isolation",
 "title": "Two gyms, one database",
 "recorded": "2026-09-26T14:33:42.843Z",
 "env": "JABBO API + Postgres 16, local copy, mock gyms",
 "steps": [
  {
   "at": 73,
   "actor": "Gym A · owner",
   "say": "Signs in to Gym A's console.",
   "req": "POST /api/auth/login",
   "body": {
    "email": "owner@jabbo.test",
    "password": "••••••••",
    "gymSlug": "gym-a"
   },
   "status": 200,
   "ms": 71,
   "res": {
    "role": "owner"
   }
  },
  {
   "at": 82,
   "actor": "Gym A · owner",
   "say": "Opens the member list. Only Gym A's members come back.",
   "req": "GET /api/gym/members",
   "status": 200,
   "ms": 8,
   "res": {
    "total": 422
   }
  },
  {
   "at": 137,
   "actor": "Gym A · owner",
   "say": "Has the id of a member who belongs to Gym B.",
   "note": "Assume the worst: the id leaked.",
   "res": {
    "id": "…9b6e"
   }
  },
  {
   "at": 140,
   "actor": "Gym A · owner",
   "say": "Asks for that member directly.",
   "req": "GET /api/gym/members/…9b6e",
   "status": 404,
   "ms": 3,
   "res": {
    "error": "no_such_member"
   },
   "verdict": "refused"
  },
  {
   "at": 190,
   "actor": "Gym B · owner",
   "say": "The same request from Gym B's own console.",
   "req": "GET /api/gym/members/…9b6e",
   "status": 200,
   "ms": 6,
   "res": {
    "status": "active"
   },
   "verdict": "allowed"
  },
  {
   "at": 247,
   "actor": "Database",
   "say": "Inside Gym A's scope, the row does not exist.",
   "sql": "set app.gym_id = Gym A; select count(*) from memberships where id = …",
   "res": {
    "count": 0
   },
   "verdict": "refused"
  },
  {
   "at": 293,
   "actor": "Database",
   "say": "A query that forgets to name its gym sees nothing, not everything.",
   "sql": "select count(*) from memberships  -- no gym set",
   "res": {
    "count": 0
   },
   "verdict": "refused"
  }
 ]
}