The Shape of an Owned App
For openGym, the question is not only where the application runs but which layer owns each decision and each piece of state. This chapter follows a normal signed-in interaction from the browser or phone to the self-hosted service, then separates the durable profile from live sessions, media, credentials, and operator-controlled files. The goal is a practical system model: readers should be able to locate behavior, understand why the boundaries exist, and see what they—not the project—must operate.
A Request Has an Owner at Each Layer
A self-hosted openGym instance is intentionally small. The normal server arrangement has a web container, an API container, a one-time service that downloads exercise media, and directories mounted from the host. That size is not just a deployment convenience. It makes the route from a user action to a durable record relatively visible.
A browser first reaches the web container. The web layer serves the React application as static files and proxies API requests to the Node API. The browser therefore sees one application origin rather than treating the interface and its data service as unrelated sites. That matters to openGym’s default authentication method: a passkey is bound to the hostname and origin for which it was created. If the page and API were casually split across origins, the application would be fighting the rules of WebAuthn rather than merely changing a network diagram.
The web container does not own a person’s training history. Its job is to deliver the interface, pass requests onward, and apply the request-size and proxy rules that make those requests possible. The API is the component that authenticates the caller, identifies the profile, reads the profile document, and accepts or rejects a state change. The host-mounted data directory is where the durable copy resides. The operator can back it up, inspect it, move it, or accidentally expose it; that last possibility is part of the trust model, not a detail the application can wish away.
The boundary is easier to understand through a simple request. Suppose a signed-in user changes a routine on a laptop. The browser updates its local working state and schedules a request. The web layer forwards the JSON request to the API. The API resolves the session to a profile and writes that profile’s state document using a temporary file followed by a replacement, rather than editing a shared database row in place. The resulting file is the server-visible copy of the profile. A phone connected to the same profile does not receive a magical stream of UI events; it later checks the server’s revision and obtains the state it needs.
This design has a clear benefit for a small installation: the durable state is a file with an understandable location and format, not an opaque service that can be recovered only through its original vendor. It also creates clear costs. A profile document can contain a substantial history, so ordinary synchronization sends the whole profile in one request. The API and bundled web layer enforce a five-mebibyte limit for that path. The design is therefore easy to deploy and reason about at modest scale, but it is not evidence of unlimited throughput or high-availability behavior. The project’s roadmap plans a later database migration while retaining the state import/export format and the revision/merge contract, which shows that the current simplicity is a stage in the product’s lifecycle rather than a claim that file storage has no limits.
The phone changes the route without changing the core question. In local mode, the phone’s private storage is the durable boundary and there is no server request at all. In paired mode, the phone has a local working copy and a bearer token for the self-hosted API. The WebView cannot use the server’s passkey ceremony directly because its origin is different, so an already signed-in browser creates a short-lived pairing code. That code establishes the connection; later requests identify the phone with the same signed session value in an authorization header. The choice of transport is different, but the question is the same: which profile is this device allowed to represent, and where does its durable state live?
The operator sits outside these ordinary request steps but controls the conditions under which they succeed. They choose the public hostname, provide or configure the reverse proxy, terminate HTTPS, preserve the authorization header for paired phones, and decide how the host filesystem and backups are protected. openGym can enforce an origin check or reject a malformed state, but it cannot make a misconfigured proxy route an API request correctly or make an exposed host private. That is why a component map without ownership is incomplete: it names services while hiding who is accountable for the transitions between them.
Separate Storage Prevents Different Kinds of State from Colliding
The profile state is not the same thing as everything the application knows at a given moment. openGym separates several categories because they have different lifecycles and different failure consequences.
The first category is durable profile data: routines, weekly scheduling, completed workouts, body-weight entries, and settings. This is the material that should follow a profile between devices and appear in a backup or export. It represents the user’s plan, record, and preferences. The API associates it with the authenticated profile rather than letting a normal user select another profile’s filename. That association is the basic data-ownership boundary.
The second category is live workout state. A workout in progress belongs to the device currently running it. It contains interaction details that may not yet represent a completed historical event. When the server stores a profile, the live active session is excluded from the persisted server document. This prevents a partially completed workout on one device from becoming an instruction that overwrites a finished record on another. It also means that sync can preserve completed history without pretending that every transient interaction is already a shared fact.
The third category is media. A custom exercise or logged workout can refer to a photo, animation, or video, but the ordinary state document carries a reference rather than embedding the binary bytes. The server stores the file under the owner’s profile directory, uses a hash-based name, checks the file type from its initial bytes, streams the upload through a hash, and applies limits for size, duration, quota, and concurrency. The API does not decode the media. This arrangement keeps large files from inflating every profile-state update and lets the server enforce ownership when a file is downloaded. It also creates an operational dependency: the operator must provide enough disk space, configure proxy limits for large uploads, and understand that files are not encrypted at rest.
The fourth category is authentication and runtime material. Public passkey records, the session-signing secret, audit information, and optional provider credentials are not interchangeable with workout history. Some provider runtime material is deliberately mounted outside the ordinary data backup path. That separation reflects different consequences if a file is copied: a workout archive is sensitive, but a refreshable provider credential can remain active wherever the archive is unpacked. Treating both as “application data” would hide that difference.
The fifth category is derived or temporary information. The application may calculate a current progress target or a displayed statistic from the durable record. It may cache state to avoid parsing a large document on every small revision poll. Those values help the application run, but they should not silently become a second source of truth. If a user edits the underlying workout history, the system needs a route to recalculate what the next session or progress view means.
The separation is not perfect isolation. The API still coordinates all of these paths, and the host operator can reach the mounted directories. But it makes the consequences of a change more intelligible. A routine edit belongs in shared profile state. A half-finished set belongs to the active device until the session is completed. A video should not force every device to carry its bytes in every state update. A session secret should not be treated like a routine. A derived number should be recomputable from the record rather than trusted because it was stored beside it.
A few ordinary failures show why these distinctions matter. If a browser sends a request and receives an HTML login page from a proxy instead of JSON from the API, the client must not interpret a successful HTTP status as proof that the state was saved. If the profile has grown beyond the proxy’s body limit, the change cannot be treated as synchronized merely because it was made locally; it remains a pending change until the boundary is corrected. If the API receives a malformed state shape, refusing it is safer than writing a document that later causes the administrator’s dashboard or reminder process to fail. These are not exotic attack scenarios. They are the everyday consequences of placing a whole editable profile behind a web boundary that may be misrouted, undersized, or temporarily unavailable.
The same reasoning explains why an active workout is not just another record waiting to be uploaded. A session in progress contains choices that can still be changed by the person standing at the equipment. A completed workout has a different status: it is historical evidence that can be merged, backed up, edited, and read by later calculations. If the server treated both as the same kind of state, a device that had merely opened a workout could overwrite a completed session or cause another device to display an unfinished plan as settled history. Keeping the active session local is therefore a semantic decision, not only a performance optimization.
The boundaries also shape what an operator should monitor. A server that is reachable but has a broken origin configuration can fail at passkey registration even though the containers are running. A reverse proxy that forwards the page but not /api can make the interface appear healthy while every data operation is going somewhere else. A disk that fills with media can eventually threaten state writes as well, because both depend on the host’s storage. A backup that includes profile state but is copied to an untrusted destination carries a different risk from a backup that excludes a live provider refresh token, even though both may be described casually as “the backup.” Ownership makes these distinctions visible; it does not automatically solve them.
There is a trade-off in the choice to keep the system inspectable. A file-backed profile is easy to copy and easy to understand for a household or a small group. It also means synchronization and recovery are organized around a whole document rather than independent database records. A large training history makes requests, parsing, proxy limits, and memory behavior more relevant. The future database milestone on the roadmap acknowledges that pressure while preserving the profile file as an import/export and backup format. That is a sensible migration boundary, but it should not be mistaken for proof that the current arrangement has already solved every scale problem.
This also explains why openGym’s architecture is better understood as a set of contracts than as a list of containers. The web layer promises to present one origin and forward requests. The API promises to authenticate and scope them, validate the state shape, and persist the profile. The client promises to keep local work when the network is absent and to recognize when a response is not actually an openGym API response. The host operator promises—whether or not the software can enforce it—to provide the network, storage, backup, and access controls that make those contracts meaningful.
The next design question follows directly. Once a routine, a completed workout, a live session, a media file, and a derived value have distinct owners and lifecycles, what does the data mean when a person starts training? openGym’s training logic answers that question by turning a routine and its history into an explainable next prescription rather than storing an opaque progression counter.