Universal Expo app targeting iOS, Android, and Web.
Stack: Expo SDK 56 · React Native 0.85 · TypeScript 6.0 · expo-router · Tailwind CSS v4 · Jest · Bun · Hono · Drizzle ORM · PostgreSQL
- Mobile App (Web): https://sonoraderivapoeticas-team-sonora.expo.app/
- Admin Portal: https://sonora-admin.expo.app/
- Mobile App (Web): https://sonoraderivapoeticas-team-sonora--staging.expo.app/
- Admin Portal: https://sonora-admin--staging.expo.app/
# Install gga (if not installed)
bun install -g ggamake installThis runs bun install and configures the git pre-commit hook.
Note: Always use
make installfor setup. Runningbun installdirectly will not configure the git hook.
make # or make start — Expo dev server (platform picker)
make dev-web # Expo dev server for web
make dev-ios # Expo dev server for iOS
make dev-android # Expo dev server for AndroidRun make help for the complete list of targets. The most common ones:
| Target | Description |
|---|---|
| Setup | |
install |
Install workspace dependencies + configure git hooks |
| Dev server | |
start |
Launch Expo dev server (default) |
dev-web |
Expo dev server for web |
dev-ios |
Expo dev server for iOS |
dev-android |
Expo dev server for Android (Expo Go) |
start-wrangler |
Dev server pointing to local wrangler (port 8787) |
start-staging |
Dev server pointing to remote staging API |
| Admin | |
admin-dev |
Launch Admin Web dev server |
| Common tasks | |
test |
Run all tests |
lint |
Run ESLint across all workspaces |
typecheck |
TypeScript type checks |
validate |
Full development gate (format → test → lint → typecheck → gga) |
help |
Print all targets |
A git pre-commit hook (.githooks/pre-commit) runs on every commit and executes these steps in order:
- format-check — Prettier checks formatting; auto-fixes and stages files if needed
- test-ci — All tests (Jest + Vitest) with silent output
- lint — ESLint across all workspaces
- typecheck — TypeScript compiler check (mobile + api + admin)
- doctor-ci — React Doctor audit
- expo-doctor — Expo dependency compatibility check (non-blocking — known false positives with Bun)
- gga — AI code review on staged files
If any blocking step fails, the commit is rejected.
The validate target (make validate: format → test → lint → typecheck → api-typecheck → scripts-typecheck → gga) can be run manually for the same development gate.
sonora/
├── apps/
│ ├── mobile/ # Expo mobile app (iOS, Android, Web)
│ │ └── src/
│ │ ├── app/ # expo-router file-based routes
│ │ ├── components/ # Reusable UI components
│ │ ├── config/ # App configuration
│ │ ├── constants/ # App constants
│ │ ├── data/ # Static data
│ │ ├── hooks/ # Custom React hooks
│ │ ├── i18n/ # Internationalization
│ │ ├── services/ # API service layer
│ │ ├── storage/ # Local storage
│ │ ├── store/ # State management (Zustand)
│ │ ├── tw/ # Tailwind utility components
│ │ ├── types/ # TypeScript type definitions
│ │ ├── utils/ # Utility functions
│ │ ├── __tests__/ # Test suites
│ │ └── global.css # Global styles
│ ├── __mocks__/ # Jest mocks (react-i18next, reanimated, etc.)
│ ├── api/ # Hono backend (Cloudflare Workers + Neon)
│ │ └── src/
│ │ ├── db/ # Drizzle ORM schema & migrations
│ │ ├── lib/ # Library code (HttpClient, etc.)
│ │ ├── middleware/ # Hono middleware
│ │ ├── payments/ # MercadoPago integration
│ │ ├── routes/ # API route handlers
│ │ ├── scripts/ # Utility scripts
│ │ ├── utils/ # Utility functions
│ │ ├── index.ts # Worker entry point
│ │ ├── server.local.ts
│ │ └── __tests__/ # Test suites
│ └── admin/ # Admin web portal (Expo)
│ └── src/
│ ├── app/ # expo-router routes
│ ├── components/ # UI components
│ ├── config/
│ ├── constants/
│ ├── hooks/
│ ├── i18n/
│ ├── services/
│ └── tw/
├── packages/
│ └── shared/ # Shared types, utilities, constants
├── openspec/ # SDD/gentle-ai artifacts
│ ├── changes/
│ ├── config.yaml
│ ├── designs/
│ ├── specs/
│ └── tasks/
├── docs/ # Documentation
├── scripts/ # Root-level utility scripts
├── scratch/ # Temporary/scratch files
├── .githooks/
│ └── pre-commit # Git pre-commit hook
├── .engram/
│ └── config.json
├── Makefile
├── package.json # Bun workspace root
├── bunfig.toml
└── renovate.json
The backend API (apps/api/) stores feedback in Postgres. Two runtimes are supported: local development with Podman, and production on Cloudflare Workers with Neon.
# 1. Start Postgres via Podman
make api-db-up
# 2. Generate and apply the initial migration
make api-db-migrate
# 3. Start the local Hono server (port 3000)
make api-dev-local
# 4. Test it
curl -X POST http://localhost:3000/feedback \
-H 'Content-Type: application/json' \
-d '{"tripId":"trip-1","message":"Great trail!","idempotencyKey":"key-1","createdAt":"2026-06-03T00:00:00.000Z"}'
# 5. Open a psql shell to inspect data
make api-db-shell
# 6. Stop Postgres when done
make api-db-down# 1. Create a Neon Postgres project (https://neon.tech)
# Copy the connection string (starts with postgres://...)
# 2. Generate and apply the migration locally first
make api-db-migrate
# 3. Set the Neon connection string as a Worker secret
cd apps/api && npx wrangler secret put NEON_DATABASE_URL
# Paste the connection string when prompted
# 4. Deploy the API to Cloudflare Workers
make api-deployThe Worker reads DB_ADAPTER=neon from wrangler.toml and connects via @neondatabase/serverless HTTP driver automatically.
| Target | Description |
|---|---|
api-db-up |
Start Postgres container (Podman) |
api-db-down |
Stop Postgres container |
api-db-generate |
Generate a Drizzle migration from schema |
api-db-migrate |
Generate + apply pending migrations |
api-db-studio |
Launch Drizzle Studio (GUI database browser) |
api-db-shell |
Open an interactive psql shell |
api-dev-local |
Run the API server locally with Postgres |
The app enforces a minimum version via remote config. On init(), the store fetches config from the API, compares the installed version (from app.config.ts → Constants.expoConfig.version) against the server's minimumVersion, and sets a versionStatus that drives conditional UI.
| Status | UI | Condition |
|---|---|---|
ok |
Normal app | Installed version >= minimum version |
warn |
UpdateWarningBanner (non-blocking, dismissible) |
Installed version < minimum, blockOlderVersions=false |
block |
UpdateRequiredModal (blocking — full-screen, no dismissal) |
Installed version < minimum, blockOlderVersions=true (and no active grace) |
When blockOlderVersions=true, a grace period can downgrade block → warn for a window defined by the server:
now >= GRACE_PERIOD_START && now < GRACE_PERIOD_END → warn (downgraded)
otherwise block
- Grace period is server-authoritative — dates are ISO 8601 strings compared server-side, and the result is included in the API response. Client does not compute grace.
- If
GRACE_PERIOD_STARTorGRACE_PERIOD_ENDis missing, no grace is applied.
Set these on the Cloudflare Worker ([vars] in wrangler.toml or wrangler.secret.toml, or wrangler secret put):
| Variable | Default | Description |
|---|---|---|
MINIMUM_APP_VERSION |
0.0.0 |
Semver minimum required version. Set to 0.0.0 to disable check |
BLOCK_OLDER_VERSIONS |
false |
When true, below-minimum users see a blocking modal |
GRACE_PERIOD_START |
(none) | ISO date start of grace window (e.g. 2026-06-25) |
GRACE_PERIOD_END |
(none) | ISO date end of grace window (e.g. 2026-07-10) |
Note:
MINIMUM_APP_VERSION=0.0.0in dev bypasses the version check. In staging/production set it to the actual minimum.
The app version is 1.0.0 (hardcoded in app.config.ts). To test each state, deploy the API with different [vars] in wrangler.staging.toml:
# 1. Deploy to staging
make api-deploy-staging
# 2. Run the app pointing to staging
make start-stagingCase — ok (no banner, no modal):
MINIMUM_APP_VERSION = "0.0.0"Case — warn (banner visible):
MINIMUM_APP_VERSION = "2.0.0"
BLOCK_OLDER_VERSIONS = "false"Case — block (modal bloqueante):
MINIMUM_APP_VERSION = "2.0.0"
BLOCK_OLDER_VERSIONS = "true"Case — grace (block downgraded to warn):
MINIMUM_APP_VERSION = "2.0.0"
BLOCK_OLDER_VERSIONS = "true"
GRACE_PERIOD_START = "2026-06-25"
GRACE_PERIOD_END = "2026-07-10"Set
GRACE_PERIOD_STARTto yesterday andGRACE_PERIOD_ENDto tomorrow from today's date.
Case — grace expired (back to block):
MINIMUM_APP_VERSION = "2.0.0"
BLOCK_OLDER_VERSIONS = "true"
GRACE_PERIOD_START = "2026-06-01"
GRACE_PERIOD_END = "2026-06-20"Each change requires a redeploy: make api-deploy-staging.
| Platform | Target |
|---|---|
| iOS | Native via Expo dev client |
| Android | Native via Expo dev client |
| Web | Static output via Expo |
All workspace package.json files use exact pinned versions (no ^, ~, or *). This ensures deterministic installs and lets Dependabot correctly identify advisory status.
scripts/pin-deps.ts is a one-time script that replaces any range specifier with the exact resolved version from bun.lock.
| When | Why | How |
|---|---|---|
| Never in daily work | Renovate already creates PRs with exact versions (rangeStrategy: pin) |
— |
After a manual bun add |
You added a dependency without an exact version | make pin-deps |
| After editing package.json by hand | You accidentally added a ^ or * |
make pin-deps |
| When bootstrapping a new workspace | To pin all new dependencies cleanly | make pin-deps |
We use Renovate Community Cloud (free tier) for automated dependency updates. Configuration in renovate.json:
enabledManagers: ["bun"]— Bun-only, won't interfere with other managersrangeStrategy: "pin"— updates use exact versions (no caret)schedule: ["before 6am on Monday"]— weeklydependencyDashboard: true— visibility into all pending updates
Post-merge: install the Renovate GitHub App on the repository.
The .github/workflows/security-audit.yml workflow runs bun audit weekly (Monday 06:00 UTC) and supports manual execution via workflow_dispatch.
| Feature | Detail |
|---|---|
| Default threshold | moderate (configurable: low/moderate/high/critical) |
| Output | $GITHUB_STEP_SUMMARY with findings table |
| Issue creation | Optional, only on workflow_dispatch with create-issue: true |
| Fallback | If bun audit --format=json fails, parses plain text |