Mobile App Shell & Navigation
Mobile App Shell & Navigation
The mobile app shell for Secure Mesh is an Expo 57 / Expo Router–based application container that defines the app entry point (expo-router/entry), the (auth) and (app) route groups, a custom Reanimated-animated tab bar, deep-linking via expo-linking, and the native shell integrations (splash screen, status bar, notifications, CallKit) that host the realtime messenger features.
Purpose and Scope
This page documents the mobile application shell and navigation architecture. It covers:
- The Expo Router entry point and the route tree defined by the template-porting map
- The
(auth)route group (signup,login) and the(app)route group (chats,groups,status,calls) - The custom tab bar with UI-thread spring animations and the screen-transition animation system
- Deep linking via
expo-linkingand navigation triggered by calls/notifications (react-native-callkeep,expo-notifications) - Native shell integration:
react-native-screens,react-native-safe-area-context,react-native-gesture-handler, splash screen, and status bar - The strict single-responsibility module decomposition that shapes how navigation modules are organized
- The SDK 57 dependency pinning that constrains the shell (from the mobile
package.jsonmanifest)
Intentionally left to sibling pages: the internals of the chats/groups/status/calls screens, the authentication business logic, the shared @securemesh/realtime-core RealtimeEngine, the E2EE/crypto layer, and the web/backend stacks. This page stays at the shell-and-navigation boundary.
Source state note: no implemented mobile source files exist in this repository yet; the repository currently contains only the architecture specification. Everything below describes the shell and navigation as specified. Where the specification does not provide an implementation detail, it is explicitly marked as not found in source.
Overview
Secure Mesh is an end-to-end encrypted, real-time multimedia messaging and audio/video calling application built for ultra-high scale and zero-trust security, delivered across Web and Mobile platforms. The mobile client is an Expo 57 application using Expo Router with typedRoutes enabled, giving the shell type-checked route strings instead of stringly-typed navigation.
The shell has two jobs:
- Contain the app: own the process entry point, root layout, splash screen, status bar, safe-area handling, and provider composition (Realm DB for offline-first storage, MMKV for encrypted key-value storage, and
@tanstack/react-queryfor server-state caching). - Own navigation: define the route tree, the auth gate between unauthenticated and authenticated surfaces, the custom tab bar, and deep links that let the OS route users into the right screen (e.g., tapping an incoming-call notification).
The UI/UX is ported directly from HTML/CSS mockups, and this porting map is the route map of the app:
| Surface | Expo Router target route |
|---|---|
| Auth flows | (auth)/signup & (auth)/login |
| Chats | (app)/chats |
| Groups | (app)/groups |
| Status | (app)/status |
| Calls | (app)/calls |
| Design tokens | Core design tokens & globals.css |
This gives a clean separation: route groups as authentication boundaries ((auth) vs (app)) and one route per primary feature tab.
A second, structural constraint drives the whole shell design: all services, components, hooks, and utilities must be split into single-responsibility modules. The navigation shell is therefore designed as many small composable modules (a custom tab bar component, a splash gate, a deep-link map, provider wrappers) rather than one monolithic navigator file.
Architecture
How to read this diagram:
- Entry point —
package.jsonsets"main": "expo-router/entry", so the OS launches Expo Router, which mounts the root layout. - Root layout — composes the native shell (splash screen, status bar, safe area) and the data providers (Realm, MMKV, React Query). It is the single place where the
(auth)and(app)groups are mounted; an auth gate decides which group the user lands in. (auth)group — unauthenticated surface:signupandloginonly.(app)group — authenticated surface: a custom tab bar hosting the four primary tabs (chats,groups,status,calls).- Native shell —
react-native-screens(native-backed screen containers),react-native-gesture-handler(gesture-driven navigation), safe-area context, splash screen, and status bar are the OS-level services the navigator runs on. - Call/notification entry —
react-native-callkeep(iOS CallKit + Android ConnectionService) andexpo-notifications+expo-task-managercan surface OS-level UI (an incoming call screen, a notification tap) that routes into the(app)group through theexpo-linkingURL map.
Why this shape? Expo Router’s file-based routing means the route tree is the folder structure — the porting map directly dictates the (auth)/(app) folders, which makes the authentication boundary structural rather than a runtime check scattered through screens. The typedRoutes flag then makes every router.push(...) target a compile-time-checked literal: type errors surface at build time instead of through runtime navigation bugs.
Route Model and Navigation Structure
Route Groups as Authentication Boundaries
The route model defines exactly two route groups under the app root:
(auth)— hostssignupandlogin. These routes exist outside the tab bar and are reachable only when there is no valid session.(app)— hosts the four tab routes (chats,groups,status,calls) plus the custom tab bar layout. These routes are the authenticated, always-visible core of the messenger.
Using parenthesized route groups (a standard Expo Router convention) keeps the URL path clean (e.g., /chats rather than /app/chats in the app’s URL scheme) while still grouping files by authentication state. This is the structural auth gate: a signed-out user’s entry point is (auth), a signed-in user’s is (app).
The Five Primary Screens
The porting map fixes the tab set at five entries — one login screen, one signup screen, and four tabs:
| Route | Role in the shell |
|---|---|
(auth)/login |
Unauthenticated entry |
(auth)/signup |
Registration |
(app)/chats |
Default tab; conversation list |
(app)/groups |
Group conversation list |
(app)/status |
Status/story feed |
(app)/calls |
Call history + active-call surfaces |
Because every feature maps to exactly one route, navigation between features is always a tab switch, and navigation into a detail screen (e.g., a single conversation) would be a stack push layered on top of the tab navigator — a pattern the “tabs + stack” composition implies but whose detail screens are not specified in the source.
typedRoutes and Module Decomposition
Two plan-level conventions interact to define how navigation code must be written:
typedRoutesenabled: Expo Router generates a union type of all valid routes. Anyrouter.push("/typo")becomes a TypeScript compile error.- Single-responsibility module decomposition: a navigator cannot live in one large file. The shell must be decomposed into single-responsibility modules — e.g., a
TabBarcomponent owning only the animated indicator, alinkingmodule owning the URL map, aprovidersmodule owning context composition, and a thin_layout.tsxthat composes them.
The intent is enforced modularity: the shell’s behavior (auth gate, tab switching, deep links) is distributed across small, individually testable units rather than concentrated in a giant layout file.
Custom Tab Bar and Animation System
The plan specifies a fully custom tab bar rather than the default Expo Router tab bar, with all animations running on the UI thread via runOnUI / scheduleOnUI:
- Custom TabBar: momentum spring slide indicator — the active-tab indicator slides between tabs with a spring animation driven by Reanimated on the UI thread, giving a fluid, native-feel transition that never blocks the JS thread.
- Message bubble entry: spring slide-up + fade — list items (message bubbles) animate in with a spring slide-up combined with opacity fade.
- Buttons:
scale(0.97)kinetic press — touchable elements scale down to 0.97 on press for tactile feedback. - StatusRing: emerald pulse repeat — the status indicator uses a repeating emerald-colored pulse animation.
The stack that enables this is react-native-reanimated 4.5.3 plus react-native-worklets 0.11.3, both pinned in the mobile manifest. Reanimated 4’s worklet model moves animation logic to the UI thread, which is why runOnUI/scheduleOnUI are emphasized: tab-bar and bubble animations must not contend with JS-thread work like Realm queries or crypto operations.
Native Shell Integration
The shell rests on five native/Expo modules that handle OS-level concerns around the navigator:
| Module | Version (pinned) | Shell responsibility |
|---|---|---|
react-native-screens |
~4.26.2 | Native-backed screen containers; stack/tab transitions render natively for performance |
react-native-safe-area-context |
~5.8.0 | Insets for notched devices; the tab bar and layouts must respect safe areas |
react-native-gesture-handler |
~2.32.0 | Gesture-driven navigation (swipes, presses) wired into the navigator |
expo-splash-screen |
~57.0.5 | Controls the launch splash; the shell holds the splash until providers (Realm, crypto) are ready |
expo-status-bar |
~57.0.1 | Status bar appearance per route group |
expo-system-ui |
~57.0.2 | Root view background color / user-interface style, kept in sync with design tokens |
The splash screen is the shell’s first render gate: in an E2EE app, the root layout must typically initialize encrypted storage (MMKV) and load a session token before deciding which route group to show. The specification does not pin the exact readiness logic, so the ordering of splash-dismiss vs. auth resolution is an implementation detail not found in source.
Deep Linking and External Navigation Entry Points
expo-linking (~57.0.4) provides the URL map that connects the outside world to the route tree. Three external entry points are implied by the mobile stack:
- Launch — the OS opens the app via the configured scheme; Expo Router resolves the initial URL and mounts the matching route.
- Incoming calls —
react-native-callkeep^4.3.16 integrates iOS CallKit and Android ConnectionService, so an incoming call can present a system UI and, when answered, must navigate the user into the active-call surface under(app)/calls. The specification pins the library and its purpose but not the exact linking callbacks. - Notifications —
expo-notifications+expo-task-managerhandle push/notification taps; tapping a message notification should deep-link to the relevant conversation, again resolved through the linking map.
Because typedRoutes is enabled, the URL map’s route strings are checked against the generated route types, keeping deep-link targets consistent with the file-based route tree.
Provider Composition in the Shell Layout
The root layout doubles as the composition root for app-wide providers:
realm^20.2.0 +@realm/react^0.20.0 — offline-first message storage; the Realm provider must wrap the navigator so every screen can read conversations from local storage.react-native-mmkv^4.3.2 — encrypted KV storage (session tokens, crypto keys) backed by OS Keychain/Keystore viaexpo-secure-store; used by the auth gate to decide between(auth)and(app).@tanstack/react-query^5.101.4 — server-state caching; a query client provider wraps the route tree.react-native-gesture-handler+react-native-reanimated— gesture and animation foundations must be installed before the navigator mounts.
The single-responsibility decomposition means these providers are almost certainly composed from separate small wrapper components, though the specification does not name those files — the exact provider file layout is an implementation detail not found in source.
Core Flow
The following sequence diagram shows how the shell handles the two most important navigation flows: cold start with auth gating, and an external (call/notification) deep link landing in the (app) group.
Step-by-step walkthrough:
- Launch — the OS starts the app;
expo-router/entrybootstraps Expo Router. - Root layout mount — the layout shows the splash screen and configures the status bar via
expo-splash-screen/expo-status-bar. - Provider init — encrypted MMKV storage and Realm open before navigation decisions are made, because the auth gate and offline-first screens both depend on them.
- Auth gate — a session token read from MMKV decides the initial route:
(auth)/loginwhen signed out,(app)/chatswhen signed in. This is the shell’s central branch point. - External entry — an incoming call surfaced by
react-native-callkeep, or a notification tap routed throughexpo-notifications, is resolved by theexpo-linkingURL map and pushes the user into the relevant(app)route, with tab animations running on the UI thread.
Usage Examples
Mobile manifest: entry point and shell dependencies
The shell’s entry point and navigation-related dependencies are pinned in the mobile package.json:
{
"name": "streaming",
"main": "expo-router/entry",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"lint": "expo lint"
},
"dependencies": {
"@expo/ui": "~57.0.8",
"@realm/react": "^0.20.0",
"@tanstack/react-query": "^5.101.4",
"expo": "~57.0.9",
"expo-linking": "~57.0.4",
"expo-notifications": "~57.0.8",
"expo-router": "~57.0.9",
"expo-secure-store": "~57.0.1",
"expo-splash-screen": "~57.0.5",
"expo-status-bar": "~57.0.1",
"expo-task-manager": "~57.0.7",
"react-native-callkeep": "^4.3.16",
"react-native-gesture-handler": "~2.32.0",
"react-native-mmkv": "^4.3.2",
"react-native-reanimated": "4.5.3",
"react-native-safe-area-context": "~5.8.0",
"react-native-screens": "~4.26.2",
"react-native-worklets": "0.11.3",
"realm": "^20.2.0"
}
}
This excerpt is the contract the shell is built against: expo-router/entry as the single entry point, SDK 57–pinned Expo modules, and the navigation/gesture/animation trio (screens, safe-area-context, gesture-handler, reanimated, worklets).
Route map derived from the template-porting directive
The porting map is the canonical route table of the app:
(auth)/signup & login
(app)/chats
(app)/groups
(app)/status
(app)/calls
Core design tokens & globals.css
Every route in the shell traces back to this mapping; it is the single source of truth for the route tree, the tab set, and the design-token integration that the shell’s layouts consume.
Configuration Options
The shell’s configuration surface is defined by the pinned dependency versions and the module constraints (no runtime config files exist in the repository yet). The following table lists the options that directly shape navigation and shell behavior:
| Option | Type | Default / Pinned Value | Description |
|---|---|---|---|
main |
string | "expo-router/entry" |
Package entry point; makes Expo Router the app bootstrap |
typedRoutes |
boolean | enabled | Generates typed route strings; router.push targets are compile-time checked |
expo-router |
package | ~57.0.9 | File-based routing engine (SDK 57 range) |
expo-linking |
package | ~57.0.4 | Deep-link URL map for external navigation entry points |
expo-splash-screen |
package | ~57.0.5 | Launch splash control; held until shell providers are ready |
expo-status-bar |
package | ~57.0.1 | Status bar appearance per route group |
expo-system-ui |
package | ~57.0.2 | Root background color / UI style synced to design tokens |
react-native-screens |
package | ~4.26.2 | Native-backed screen containers for the navigator |
react-native-safe-area-context |
package | ~5.8.0 | Safe-area insets for layouts and the custom tab bar |
react-native-gesture-handler |
package | ~2.32.0 | Gesture plumbing required by the navigator |
react-native-reanimated |
package | 4.5.3 | UI-thread animations (runOnUI / scheduleOnUI) |
react-native-worklets |
package | 0.11.3 | Worklet runtime that powers Reanimated 4 on the UI thread |
react-native-callkeep |
package | ^4.3.16 | CallKit / ConnectionService OS call UI and call navigation |
expo-notifications + expo-task-manager |
package | ~57.0.8 / ~57.0.7 | Notification taps that deep-link into routes |
realm + @realm/react |
package | ^20.2.0 / ^0.20.0 | Offline-first storage provider wrapping the route tree |
react-native-mmkv |
package | ^4.3.2 | Encrypted KV used by the auth gate (session token) |
@tanstack/react-query |
package | ^5.101.4 | Server-state cache provider wrapping the route tree |
The mobile manifest pins
react,react-native, and allexpo-*packages to the SDK 57 compatible range; the guidance is to runnpx expo installto stay aligned.
Failure Modes, Edge Cases and Concurrency
SDK 57 pinning drift
The shell pins react 19.2.3 / react-native 0.86.2 and every expo-* module to the SDK 57 range. Mixing versions outside that range breaks the native shell (screens, gesture handler, reanimated) at build or runtime. Mitigation: use npx expo install to keep packages aligned.
Auth-gate race on cold start
The root layout must decide between (auth) and (app) from a token read out of encrypted MMKV. If the splash is dismissed before the token read completes, the user could be routed to the wrong group or see a flash of the login screen. The specification requires the splash to gate provider readiness but does not pin the exact sequencing — a real implementation risk to be validated.
External entry points racing the auth gate
An incoming-call action (react-native-callkeep) or notification tap can arrive before the route tree is ready. The expo-linking URL map must queue or defer the target route until the (app) group is mounted, otherwise the deep link is dropped. Not specified in source; flagged as an implementation concern.
UI-thread vs. JS-thread contention
All shell animations run on the UI thread (runOnUI / scheduleOnUI). Heavy JS-thread work (Realm writes, React Query refetches, crypto via react-native-quick-crypto) can still delay route transitions even though the tab indicator itself stays smooth. Small, focused modules that avoid blocking work in render paths mitigate this.
Concurrency of the tab indicator and route changes
The momentum spring slide indicator animates the active tab position. A rapid sequence of tab presses must be reconciled between the JS-side route state and the UI-side animation state; Reanimated’s worklet model keeps the animation uninterrupted, but cancellation semantics for mid-spring route changes are not specified.
Performance and Operational Considerations
- Native-backed navigation:
react-native-screensrenders screen containers natively, keeping tab switches and stack pushes off the JS thread. - UI-thread animation: the custom tab bar’s spring indicator and bubble entry animations run via Reanimated worklets, so they do not drop frames under JS load.
- Offline-first data: Realm provides local, instantly readable conversation data at navigation time, so tab switches into
chats/groupsdo not block on network. - Encrypted KV reads: MMKV is chosen for fast synchronous reads of session state, keeping the auth-gate decision cheap on every cold start.
- Build-time route checking:
typedRoutesmoves invalid-route detection to compilation, eliminating a class of runtime navigation crashes before release.
Extension Points
- Adding a tab — a new feature tab follows the established pattern: add a route under
(app)and register it in the custom tab bar. The porting map’s “one route per feature” convention keeps this mechanical. - Deep-link surface — the
expo-linkingURL map is the single place to register new external entry points (share links, call invites, notification payloads). - Custom TabBar — the stock tab bar is replaced with a custom, animated one; this component is the extension point for navigation chrome (badges, unread indicators, call-in-progress states).
- Provider composition in the root layout — new cross-cutting concerns (analytics, theme tokens, feature flags) plug in as additional small providers around the route tree, consistent with the single-responsibility decomposition.
Related Links
- Sibling catalog topics not covered here: Authentication & Signup Flows (
(auth)group internals), Chats / Groups / Status / Calls screen implementations, Realtime Engine (@securemesh/realtime-core), E2EE & Crypto Layer — each belongs to its own page.