In summer 2026, I worked on The Peer Network’s React Native app for booking appointments and video sessions with licensed peer counselors. The app coordinates JavaScript, native media SDKs, a REST backend, and the iOS and Android operating systems.
This article focuses on the call lifecycle in that app. An authentication timer should not interrupt a conversation by navigating to sign-in. The app still needs to enforce API session expiry.
01 / Separate call and API lifecycles
The application has two independent network paths. Its REST API handles identity, profiles, and bookings. Amazon Chime handles the actual audio and video through a local native module. Once the meeting is connected, media traffic does not go through the application’s API client.
These separate connections can cause an inactivity mismatch. A person can spend an hour actively talking while making no authenticated API requests. To the media layer, they are connected. To an API that measures inactivity from its last request, they can appear idle.
The design therefore keeps three responsibilities distinct: the meeting reports whether it is live, the token manager tracks session timing, and a central session guard decides when a forced sign-out can take effect. No individual screen needs to invent its own response to an expired token.
02 / The native video bridge
The app uses Expo, with a local module wrapping Amazon Chime on iOS and
Android. JavaScript accesses it through a typed bridge with operations such as
startMeeting, stopMeeting, setMute, and
setCameraOn. Meeting and attendee events travel back in the other
direction. Native video views render the streams inside frames laid out by
React Native.
Keeping this interface small makes the ownership clear. React Native owns the screen and layout; the native SDK owns media. The wrapper also exposes whether the native bridge is available. If it is absent, the meeting screen shows an unavailable state instead of presenting controls for a call it cannot start. Expo Go cannot load this custom native module; a development build is needed for working video. Expo documents this native-code boundary.
The same separation applies to event timing. Mounting the meeting screen does
not mean a call is live. Joining may fail, permissions may be missing, or the
SDK may never connect. The session guard becomes call-active on
OnMeetingStart and becomes inactive on
OnMeetingEnd or OnError. An indefinitely mounted
“Joining…” screen therefore cannot suspend inactivity handling forever.
A video tile can exist before it has valid dimensions. The SDK can announce a tile before its first frame, with dimensions of zero. The self-view follows subsequent size-change events so that the first frame and later device rotations can update its aspect ratio. Explicit aspect-fit rendering on both platforms prevents a mismatch between the incoming stream and the available frame from stretching or cropping the picture.
03 / Session timers
In the documented backend contract for this app, tokens have a 15-minute idle window and an eight-hour absolute lifetime. Those are separate limits. A keepalive can maintain activity; it cannot move the absolute deadline. The client also needs its own view of user activity, because scrolling cached content might never produce a request.
| Mechanism | When it runs | What it means |
|---|---|---|
| API keepalive | Every 5 minutes; also at call start and foreground | Maintain the server’s idle window while the user is active. |
| Client inactivity timer | 15 minutes since user activity | Sign out an inactive user; suspend while a call is live. |
| Absolute-expiry watchdog | At the server-stated expiry | The API session has reached its cap. Defer the UI sign-out during a live call. |
The client tracks keepalives separately from user activity. If it reset the client inactivity timer, the application would keep itself signed in merely by running. Instead, pings are gated on recent user activity or a live call. When a call ends, the inactivity window starts again from that point: a quiet participant has still been using the app.
Token rotation is another operation with a different purpose. In this backend, refresh rotates the bearer token but carries its original expiry forward. It cannot extend the session. Repeatedly refreshing near expiry would add requests and revoke tokens that other requests might still be using, without extending the session.
Deferring sign-out does not renew an expired token. The API can reject that token while the existing Chime media session continues. The guard delays the navigation and teardown until the call ends; it does not override server authorization.
Foreground transitions matter too. A mobile operating system can freeze JavaScript timers in the background. On return, the manager checks the deadline against wall time and sends a keepalive immediately when appropriate. A timer callback alone is not proof that a deadline has been enforced.
There is also a recovery case: a successful authenticated keepalive during a call cancels a previously deferred sign-out. That successful response is stronger evidence of the current API session than the earlier failure that queued the sign-out.
04 / Handling call exits
Several events can end a call. The user can press End, use Android’s back button, receive a remote meeting-end event, or encounter an SDK error. Two of those events can arrive almost together. Without a shared guard, the app can stop the meeting twice or navigate twice.
The meeting screen funnels exits through one guarded operation. The once-only guard is set before teardown, because teardown itself may produce another end event. The following is illustrative pseudocode for that ordering, rather than a copy of the production implementation:
function leaveMeeting(reason) {
if (exitStarted) return;
exitStarted = true;
// Preserve a failure if a later end event follows it.
const finalReason = failed ? "failed" : reason;
stopMediaAndRemoveListeners();
handOffOrReturn(finalReason);
}
The destination is a typed, serializable navigation target supplied by the caller. That keeps the video screen from deciding product policy about what should happen after a session. When a post-session destination is supplied, it replaces the meeting route. Pressing Back from that destination should not reopen a finished call. This uses the stack replacement behavior described in the React Navigation reference.
The leave reason travels with the handoff. A voluntary departure is
left; a normal remote end is ended. A failure stays
failed even if the SDK subsequently emits a normal end event. If
an end arrives while the app is backgrounded, it is classified as
interrupted. That gives the next screen enough context to avoid
declaring success after a call that never connected.
Some exits deliberately behave differently. An error remains visible until the user chooses to leave, so navigation does not erase the explanation. The iOS edge-swipe is disabled on the meeting route so it cannot bypass controlled teardown. If a post-session handoff is rejected, the app falls back to returning to the previous screen. Leaving must remain possible even when the destination fails.
Backgrounding alone is not an end event. The documented Android implementation is foreground-only and does not run a foreground service. Local capture can stop when the app is backgrounded; returning does not explicitly restart the camera or unmute. Whether capture recovers depends on the SDK. This architecture does not promise uninterrupted background calls.
05 / Handling stale responses
There is one more race outside the video screen. A keepalive starts for user
A. Before it returns, A signs out and user B signs in. By the time A’s
response arrives, the token manager is running again. Checking a boolean such
as running would accept a response from the wrong session.
The manager therefore carries a run epoch: a counter that changes on each start and stop. An asynchronous operation captures that epoch and checks it again after awaiting a response. It also captures the token it made the request with. An expiry update is allowed only if the stored session still belongs to that token.
// Illustrative: associate async work with its session.
const epochAtStart = runEpoch;
const tokenAtStart = currentToken;
const status = await readStatus(tokenAtStart);
if (epochAtStart !== runEpoch) return;
await updateStoredExpiry(status.expiresAt, {
forToken: tokenAtStart
});
The token comparison belongs inside the serialized storage write, atomically with that write. Checking before enqueueing it leaves another window in which sign-out can clear storage. A late write could otherwise restore a session after the interface has already returned to sign-in.
This is the broader pattern: an asynchronous result must establish both that it succeeded and that it still belongs to the state it intends to change. The same reasoning applies to a profile request that resolves after sign-out and must not put the app back into the authenticated navigation tree.
06 / Verification
The useful checks target transitions between subsystems. These are the cases I would use to evaluate this design; they are a verification checklist, not a claim that every scenario below was measured on every device:
- Joining never connects. The mounted screen must not keep the session marked call-active.
- The API session expires mid-call. API authorization still fails; the existing media call is not interrupted by a navigation bounce. Deferred sign-out takes effect when the call ends.
- End and a native end event arrive together. There is one teardown and one handoff, with the original failure preserved if relevant.
- A user switches accounts during a request. The old response cannot alter the new user’s expiry or auth state.
- The app backgrounds and returns. Re-check wall-clock expiry and observe real two-party capture recovery on each platform.
- The process is killed. Do not expect JavaScript cleanup or a post-session navigation event to run.
That final case marks an actual limit. An OS kill is not another callback to handle. Recovery has to come from durable booking history or a server-backed entry point, not from assuming an in-process exit handler always gets a turn.
Each subsystem has a specific responsibility. The SDK reports the meeting state. The backend defines the token’s lifetime. The navigation layer owns the route. The client coordinates those facts, and rejects late events that no longer belong to the current session. That is what makes a simple Join or Leave button dependable.
References & scope
This account is based on The Peer Network project’s architecture, native-video, navigation, and data-fetching documentation, provided as reference for my summer 2026 work. It describes the documented design, including its limitations. Code snippets above are simplified explanations; internal source files and project documents are not reproduced here.
- Expo: Add custom native code — why this module requires a native development build.
- React Navigation: StackActions.replace — replacing the completed meeting route.
More about my work at The Peer Network, or get in touch.
← All articles