EGCA EGCA Engineering Handbook
Internal reference · v1
Home Onboarding Git Review Prototype ↔ Prod
Home Playbooks Mobile & CLI

Playbook — Mobile & CLI

Two small-surface-area project types grouped together because the advice is short.


Mobile

Stack

  • Expo + React Native + TypeScript + Expo Router. File-system routing, shared JS talent, OTA updates.
  • Native code only when platform APIs require it — Bluetooth, background audio, deep OS integration. Otherwise Expo handles everything.
  • EAS Build for binaries. EAS Update for OTA JS-only updates (bug fixes without app store review).

Why Expo and not plain React Native?

  • OTA updates. Huge for incident response — ship a fix in minutes, not a week.
  • EAS handles certs / provisioning / signing. Juniors don’t touch Xcode for a standard app.
  • Prebuild ejects cleanly if you ever need to. You’re not locked in.

Project shape

app/                    # Expo Router — file-system routing
  (auth)/
    sign-in.tsx
  (tabs)/
    index.tsx
    settings.tsx
components/
lib/
  api/                  # Client for your backend
  auth/
__tests__/
  smoke.test.ts
maestro/
  sign-in.yaml

State & data

  • React Query for server state (the backend API).
  • Zustand for local UI state that crosses screens.
  • MMKV (react-native-mmkv) for persistent key-value (tokens, prefs). Faster than AsyncStorage.
  • Forms: React Hook Form + Zod. Native input ergonomics make RHF worth it here.

Auth

  • Client-facingClerk Expo SDK. Middleware + hooks handle session.
  • Internal staff app (rare but it happens) → MSAL for React Native with the @egca.io tenant. Same policy as web — see § Architecture — Auth. Never Entra for a client-facing mobile app.
  • Paired with our own backend → short-lived JWT issued by the backend after the initial Entra or Clerk login. Rotate on app resume.

Networking

  • Single apiClient in lib/api/ using fetch + a thin wrapper for auth headers + error mapping.
  • Don’t install axios. fetch is fine in 2026.
  • Retry with backoff on transient failures. Don’t retry on 4xx.

Offline & sync

  • Most apps don’t need real offline. Don’t build it speculatively.
  • If you do — TanStack Query’s persistence, or PowerSync / Replicache for proper sync. Not a DIY queue.

Styling

  • NativeWind (Tailwind for RN). Consistent with our web stack, less context-switching.
  • Design tokens shared with web via a @company/tokens package if you’re in a monorepo.

Testing

  • Jest + React Native Testing Library for unit/component.
  • Maestro for E2E — YAML flows, fast, easy to write.
  • Minimum: smoke test + one Maestro flow (sign-in → home). See 04-testing.

Distribution

  • Internal distribution via EAS for every PR — QA gets a real build to poke at.
  • App Store / Play Store via EAS Submit. Automate the upload; don’t click through Apple’s UI by hand.

CLI / scripts

Stack

  • TypeScript + tsx + commander for CLIs consumed by JS devs or non-technical ops users.
  • Python + typer for CLIs consumed by data/ML folks or anything that touches pandas.
  • Pick by your audience, not by your preference.

When a CLI is the right answer

  • A task you’ll run > 3 times.
  • Something ops/support needs to do without bothering engineering.
  • A reproducible piece of automation. “Re-sync all client X’s invoices” belongs in a CLI, not a Teams DM.

Project shape (TypeScript)

src/
  commands/
    sync.ts
    cleanup.ts
  lib/
    db.ts
  cli.ts            # commander entry point
tests/
  smoke.test.ts

Entry point:

#!/usr/bin/env tsx
import { Command } from 'commander'
const program = new Command()
program.command('sync').action(run)
program.parse()

Project shape (Python)

src/company_cli/
  commands/
    sync.py
  lib/
    db.py
  main.py           # typer app
tests/
  test_smoke.py

Distribution

  • Most internal CLIs: just commit the repo + README with run steps. No packaging needed.
  • When you need a single binary (ops machine, no Node/Python runtime):
    • TS → bun build --compile or pkg.
    • Python → PyInstaller or uv build.
  • Don’t package until someone asks for it.

What NOT to do

  • Don’t build a CLI “framework.” commander / typer are already the framework.
  • Don’t add config files for a 3-flag CLI. Environment variables + flags are fine.
  • Don’t invent a plugin system. If you have 2 commands, it’s a CLI, not a platform.
  • Don’t skip tests because “it’s just a script.” A CLI that silently corrupts data is worse than a web app that throws a 500.

Config & secrets in CLIs

  • .env loaded via dotenv (TS) or python-dotenv (Python), same pattern as apps.
  • For prod-facing CLIs: read secrets from Azure Key Vault at runtime, not baked into env files.
  • Dry-run flag by default on anything destructive. --yes to actually execute.

Auth for internal CLIs

  • If a CLI talks to an internal API that’s behind Entra, use the Azure CLI device-code flow: az login --tenant egca.io then az account get-access-token --resource <api-id> from inside the CLI.
  • Alternatively, the Azure SDK’s DefaultAzureCredential picks up az login creds automatically in TS and Python.
  • Never hand out long-lived service-principal secrets to devs. One service principal per CLI, stored in Key Vault.

Logging

  • Structured logs (pino TS / structlog Python) to stdout, JSON.
  • Progress output to stderr so stdout can be piped to another command without noise.
  • Don’t use console.log for anything serious. It’s not a script you’ll delete tomorrow if it’s committed.