Skip to main content

OdontoX Mobile App — Phase 1 Design Spec

Date: 2026-05-06
Status: Draft
Scope: Phase 1 — end-to-end functional, essentials only
Roles: Patient · Doctor · Admin · Receptionist (Superadmin: Phase 2)

1. Goals

Build a single Expo SDK 55 binary (iOS + Android) that mirrors the role-based access model of go.odontox.io at a lean Phase 1 scope. Zero new backend infrastructure — the app runs entirely against the existing Hono/Cloudflare Workers API with JWT auth. Non-goals (Phase 1):
  • Dental chart
  • Inventory, Lab, Payroll, Insurance
  • AI/Ruby features
  • Bridge (imaging)
  • IPD
  • Superadmin
  • Audit logs

2. Tech Stack

LayerChoiceReason
FrameworkExpo SDK 55 (RN 0.83, React 19.2)Cross-platform, lowest cost, OTA updates
ArchitectureNew Architecture only (Fabric + JSI)Mandatory in SDK 55 — no legacy arch
RouterExpo Router v7 + native-tabsPlatform-native tab bars (UITabBarController on iOS, NavigationBar on Android)
Native UI@expo/ui SwiftUI (iOS) + Jetpack Compose beta (Android)True platform-native components, not JS shimmed
StylingNativeWind v4Tailwind syntax for shared layout
Auth storageexpo-secure-storeKeychain (iOS) / Keystore (Android)
Biometricsexpo-local-authenticationFace ID / Touch ID / Biometric Prompt
Encryptionexpo-crypto (AES-GCM)Client-side PHI field encryption, no native module needed
Pushexpo-notifications + EASAPNs + FCM via single API
HTTP clientfetch + React Query v5Cache, mutations, background refetch
StateZustandAuth session only
PDF/Filesexpo-file-system + expo-sharingSigned URL download → share sheet
iPad layoutSplitView (Expo Router v7, experimental)Native sidebar + detail pane
Build & OTAEAS Build + EAS Update75% smaller OTA via Hermes bytecode diff
BackendExisting Hono/CF Workers (/api/v1/*)Zero changes to server

3. System Architecture


4. Auth Flow

Rules:
  • JWT stored in SecureStore, never AsyncStorage
  • mPIN validated locally (no network round-trip)
  • Biometric prompt wraps mPIN path — same code
  • Session flushed on app backgrounded > 15 min (configurable)
  • Concurrent session detection: server returns ConcurrentSession 401 → force logout
  • Refresh token rotation on each refresh call

5. Mobile Permission Engine

A lightweight layer that gates tabs, screens, and action buttons on mobile. Separate from (but derived from) the web permission system.

5a. New DB table (one migration)

CREATE TABLE mobile_role_permissions (
  id          uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  clinic_id   uuid NOT NULL REFERENCES clinics(id) ON DELETE CASCADE,
  role        text NOT NULL,          -- 'doctor' | 'admin' | 'receptionist' | 'patient'
  module      text NOT NULL,          -- e.g. 'appointments', 'clinical.notes'
  enabled     boolean NOT NULL DEFAULT true,
  updated_at  timestamptz NOT NULL DEFAULT now(),
  UNIQUE(clinic_id, role, module)
);
Seeded with Phase 1 defaults on clinic creation.

5b. API endpoint (new, minimal)

GET /api/v1/mobile/permissions
Returns the calling user’s enabled module set for their clinic + role. Cached on device for 24h; refreshed on login and on app foreground after 1h.

5c. Web admin control

Existing Settings → Staff page gains a “Mobile Access” toggle panel per role. Superadmin can override per-clinic from their clinic detail view. No new pages — extends existing settings UI on the web app only. The mobile app shows a read-only view of current mobile permissions for the admin to reference.

5d. App usage

// usePermissions() hook
const { can } = usePermissions();
can('appointments')       // → boolean
can('clinical.notes')     // → boolean
Tabs and action buttons hidden (not just disabled) when can() returns false.

6. Module Matrix — Phase 1

ModulePatientDoctorAdminReceptionist
Appointments — view, create, statusown only
Patients — list, detail, createown profile
Clinical Notes
Treatment Plans — view + createview own
Prescriptions — view + createview own
Medical Records / Filesview own
Vital Signsview own
Billing — Invoices & Receiptsview own
Payments — record + view
Dashboard Statsbasicfull
Messaging (patient comms)
Notifications
Profile & Security
All staff modules controlled by mobile_role_permissions — admin can disable any per-clinic from web.

7. Navigation Structure

iOS: Native UITabBarController via Expo Router v7 native-tabs — SF Symbols, zero JS shimming
Android: Material 3 NavigationBar via Jetpack Compose beta — dynamic color theming
iPad: SplitView (Expo Router v7) — native sidebar on left, detail on right
Transitions: Apple Zoom Transition (shared-element) enabled by default on iOS

8. Screen Inventory

8a. Shared / Auth

ScreenRoute
Login/auth/login
Set mPIN/auth/set-pin
Enter mPIN/auth/pin
Select Clinic (multi-clinic)/auth/select-clinic
Notification permission/auth/notifications

8b. Patient

ScreenRoute
Home (next appt, quick links)/(patient)/
Appointments list/(patient)/appointments/
Book appointment/(patient)/appointments/book
Appointment detail/(patient)/appointments/[id]
Medical records list/(patient)/records/
Treatment plan detail/(patient)/records/treatment/[id]
Prescription detail/(patient)/records/prescription/[id]
File viewer (PDF/image)/(patient)/records/file/[id]
Invoice list/(patient)/bills/
Invoice detail/(patient)/bills/[id]
Chat inbox/(patient)/chat/
Chat thread/(patient)/chat/[id]
Profile/(patient)/profile
Notifications/(patient)/notifications

8c. Doctor

ScreenRoute
Home (today’s schedule, stats)/(doctor)/
Appointment list/(doctor)/appointments/
Appointment detail + actions/(doctor)/appointments/[id]
Patient search/(doctor)/patients/
Patient profile/(doctor)/patients/[id]
Clinical notes list/(doctor)/patients/[id]/notes
Add/edit note/(doctor)/patients/[id]/notes/[noteId]
Treatment plans/(doctor)/patients/[id]/treatment
Add treatment plan/(doctor)/patients/[id]/treatment/new
Prescriptions/(doctor)/patients/[id]/prescriptions
New prescription/(doctor)/patients/[id]/prescriptions/new
Patient files/(doctor)/patients/[id]/files
Vital signs/(doctor)/patients/[id]/vitals
Chat inbox/(doctor)/chat/
Chat thread/(doctor)/chat/[id]
Profile + settings/(doctor)/profile
Notifications/(doctor)/notifications

8d. Admin

All doctor screens (same routes under /(admin)/) plus:
ScreenRoute
Stats dashboard/(admin)/
Invoice list/(admin)/finance/invoices
Invoice detail/(admin)/finance/invoices/[id]
Create invoice/(admin)/finance/invoices/new
Receipt list/(admin)/finance/receipts
Record payment/(admin)/finance/payment/new
Mobile permissions view (read-only)/(admin)/settings/mobile-permissions

8e. Receptionist

ScreenRoute
Home (check-in queue)/(receptionist)/
Appointment list/(receptionist)/appointments/
Appointment detail/(receptionist)/appointments/[id]
New appointment/(receptionist)/appointments/new
Patient search/(receptionist)/patients/
Patient profile (read)/(receptionist)/patients/[id]
New patient/(receptionist)/patients/new
Invoice list/(receptionist)/finance/invoices
Create invoice/(receptionist)/finance/invoices/new
Record payment/(receptionist)/finance/payment/new
Chat inbox/(receptionist)/chat/
Chat thread/(receptionist)/chat/[id]
Profile/(receptionist)/profile
Notifications/(receptionist)/notifications

9. API Integration

All calls use the existing https://api.odontox.io/api/v1/ base. No new endpoints except GET /api/v1/mobile/permissions and the seeding hook on clinic creation. Error handling:
  • 401 → attempt silent refresh → if fails, force logout to /auth/pin
  • 403 → show “Access denied” inline, no crash
  • ConcurrentSession code → force logout with message
  • Network offline → React Query stale cache shown with banner

10. Push Notifications

  • On login: register expo-notifications token → POST /api/v1/user-devices (existing endpoint)
  • Notification types Phase 1: appointment reminders, new message, payment received (patient), appointment status change
  • Deep link on tap: odontox://appointments/[id], odontox://chat/[id]

11. File & PDF Handling

Documents (prescriptions, invoices, records) are served from existing R2 via signed URLs from the API. Flow:
  1. App calls existing file endpoint → receives signed URL (TTL 5 min)
  2. expo-file-system downloads to temp cache
  3. Rendered in-app via a WebView-based PDF renderer (expo/use-dom + PDF.js) for inline viewing, or passed to expo-sharing for system share sheet
  4. Temp files purged on app background
No files stored permanently on device.

12. Security

ConcernMitigation
Token storageexpo-secure-store only (Keychain/Keystore)
Screenshot preventionFLAG_SECURE on Android via native module
Session idleAuto-lock after 15 min background, configurable
Biometric re-authRequired on return from background
HTTPS onlyATS enforced (iOS), cleartext blocked (Android)
mPIN brute force5 attempts → force full re-login
Concurrent sessionsServer-enforced, ConcurrentSession 401
PHI in logsNo patient data in console/Crashlytics
PHI field encryptionexpo-crypto AES-GCM for sensitive fields in local cache

13. Design System

  • iOS: @expo/ui SwiftUI components — Button, TextField, Picker rendered as real SwiftUI views. SF Symbols throughout. Native UITabBarController via native-tabs.
  • Android: @expo/ui Jetpack Compose (beta) — dynamic Material 3 color theming synced to device wallpaper. Material icons.
  • Shared: NativeWind v4 (Tailwind syntax for layout), brand color #5048E5 (indigo), adaptive dark/light mode
  • Component library: Built fresh — no dependency on legacy Atelier system
  • Industry pattern: Bottom tab navigation is the universal dental DMS mobile standard (Curve Dental, CareStack, NexHealth all follow this). Clinical charting intentionally desktop-only (consistent with market leaders).

14. Project Structure

odontox-app/                    ← new directory, not mobile/
├── app/
│   ├── _layout.tsx             ← root layout, auth gate
│   ├── auth/
│   │   ├── login.tsx
│   │   ├── pin.tsx
│   │   ├── set-pin.tsx
│   │   └── select-clinic.tsx
│   ├── (patient)/
│   │   ├── _layout.tsx         ← patient tab layout
│   │   ├── index.tsx
│   │   ├── appointments/
│   │   ├── records/
│   │   ├── bills/
│   │   ├── chat/
│   │   └── profile.tsx
│   ├── (doctor)/
│   │   ├── _layout.tsx
│   │   └── ...
│   ├── (admin)/
│   │   ├── _layout.tsx
│   │   └── ...
│   └── (receptionist)/
│       ├── _layout.tsx
│       └── ...
├── components/                 ← shared UI components
├── hooks/                      ← usePermissions, useAuth, useQuery wrappers
├── services/                   ← API client functions
├── store/                      ← Zustand auth store
├── lib/
│   ├── api.ts                  ← base fetch client, JWT injection, refresh logic
│   ├── secure-storage.ts
│   └── permissions.ts
├── constants/
│   └── theme.ts                ← brand colors, typography
├── app.json
└── eas.json

15. Backend Changes (minimal)

ChangeScope
mobile_role_permissions tableNew migration, ~10 lines
GET /api/v1/mobile/permissionsNew route, ~30 lines
Seed defaults on clinic creationHook in existing clinic create handler
Web admin: Mobile toggles in SettingsExtends existing staff settings UI
POST /api/v1/user-devicesAlready exists — no change
Everything else on the backend is untouched.

16. Build & Distribution

StageTooling
Local devexpo start + Expo Go / Dev Client
CI buildsEAS Build (cloud)
TestFlight (iOS)EAS Submit
Play Store (Android)EAS Submit
OTA updatesEAS Update (JS-only changes, no store review)
Env secretsEAS Secrets (never in repo)

17. Phase 2 Backlog

  • Dental Chart (interactive)
  • Inventory
  • Lab Cases
  • AI/Ruby features
  • Bridge imaging viewer
  • Superadmin shell
  • IPD
  • Payroll & Insurance
  • Offline mode (full sync)