Point your AI coding agent at this repo, describe an idea, and it ships a real, live app to AWS, with no infrastructure to build.
Works with Claude Code · Codex · Cursor · Windsurf · Cline
spec-driven gate · 0 stored keys · native call/SMS · MIT
Clone it, drop in your app and your service, and inherit CI/CD, infrastructure as code, security scanning, native call/SMS module references, OIDC deploys with zero stored credentials, and a spec-driven test gate that refuses to ship an app whose requirements are not proven.
This is the mobile sibling of the web platform template. Mobile and NestJS belong here; web-only concerns live in the other one. The live showcase features the whole family, the apps below plus the web ones, all openable right now.
A full reconstruction of Singapore's ScamShield runs on this template: a React Native app, a NestJS API on AWS, a Postgres store, and an admin verification dashboard, with call blocking, SMS filtering, check-and-report, push, SQS report intake, and OpenSearch clustering. Try the web build or read the repo. CI also builds the demo Expo app and NestJS service and runs the spec gate against the construct on every push.
Point Claude Code, Codex, Cursor, Windsurf, or Cline at this repo and describe an app: it scaffolds the app, the service, and the infra from the templates, builds, and ships. The agent conventions live in CLAUDE.md, including a mandatory spec-first build protocol.
One command wires the API's cloud connection so every push deploys with no stored keys:
npm run setup # scripts/connect.sh
npm run setup -- --dry-run # preview without changing anythingIt ensures the GitHub OIDC provider, deploys a least-privilege deploy role, provisions a database, generates JWT_SECRET, and sets every GitHub Actions secret and variable. The agent guides you through the interactive steps (database choice, and EAS login for the app), the AWS/GitHub half is automated.
- Ships a per-app clone target, not a dependency: an Expo app overlay, a NestJS service scaffold, a CDK package, and a one-time AWS setup stack, all copied and renamed per app. Each app pins its own copy, so a breaking change never propagates without explicit action.
- Surfaces native call blocking and SMS filtering through Expo config plugins: iOS Call Directory and Message Filter extensions (Swift), Android CallScreeningService and SMS role (Kotlin). The JS layer manages data; the OS does the interception.
- Provides a reusable
NestjsApiCDK construct: HTTP Lambda behind API Gateway, an SQS report-intake queue with a dead-letter queue, an idempotent worker Lambda, and an optional OpenSearch domain. - Runs four GitHub Actions workflows: CI (typecheck, lint, expo-doctor, nest build, cdk synth, spec gate), security (CodeQL, gitleaks, npm audit), mobile build (EAS build, submit, OTA update), and API deploy (OIDC, CDK deploy, smoke test).
- Enforces a spec-driven test gate: every requirement is covered by a passing, asserting test or a fresh signed real-device artifact, or the build fails.
- Ships least-privilege IAM (a deploy policy and an OIDC role) so the deploy role is never
AdministratorAccess. - Dogfoods all of the above through
apps/_demo/andservices/_template/, built by the same workflows an app inherits.
flowchart TD
app["Expo app (React Native + Expo Router, TS strict)"]
plugins["Config plugins (app.config.ts)"]
native["Native extensions: iOS Swift, Android Kotlin"]
api["NestJS API (HTTP Lambda)"]
queue["SQS report-intake queue + DLQ"]
worker["Worker Lambda (idempotent consumer)"]
classifier["Classifier (LLM with heuristic fallback)"]
pg["PostgreSQL (Neon serverless)"]
search["OpenSearch (optional clustering)"]
push["Push (Expo: APNs / FCM)"]
app --> plugins
plugins --> native
app --> api
api --> queue
queue --> worker
worker --> classifier
worker --> pg
worker --> search
worker --> push
The native extensions run out of process and intercept calls/SMS at the OS level; the app reaches them only through documented config-plugin extension points.
What one app deployed on the NestjsApi construct looks like in AWS:
flowchart TD
device["Mobile device (iOS / Android binary from EAS)"]
apigw["API Gateway HTTP API"]
http["HTTP Lambda (lambda.handler, arm64)"]
q["SQS ReportsQueue"]
dlq["ReportsDlq (maxReceiveCount 5)"]
wkr["Worker Lambda (worker.handler, arm64)"]
neon["Neon PostgreSQL"]
os["OpenSearch domain (optional)"]
device --> apigw
apigw --> http
http --> q
q --> wkr
q --> dlq
wkr --> neon
wkr --> os
Every app on this platform is built from a spec and tested against it. The first artifact for any app is specs/<app>.yml: each requirement gets a unique ID, a category, a severity, a verify level, and a given/when/then. Tests name themselves with the requirement ID in brackets; a per-runner recorder parses the [ID] and records pass or fail. The runner is @platform/spec-test, dual-published as ESM and CJS so Vitest (API), jest-expo (app unit and component), Maestro (app e2e), and Node CLIs all consume it.
The gate (spec-coverage) parses the spec, reads the coverage record, evaluates any native/manual signed artifacts, and exits non-zero on any gap. It catches a missing test, a covering test that fails, a [ID] test with zero expect() calls (an ESLint rule fails before tests run), a requirement proven in the wrong layer, and a native/manual requirement whose signed artifact is missing, unsigned, tampered, or stale.
OS-level behavior (call blocking, SMS filtering) cannot have a JS test, because the interception runs out of process. Those verify: native | manual requirements are proven instead by signed real-device artifacts under verification/, with two layers: an in-file sha256 stamped by spec-attest (tampering is caught), and the GPG/SSH signature on the commit that last touched the artifact (accountability, enforced by scripts/verify-attestations.sh). Artifacts go stale on app-version bumps, OS baseline drift, or a 90-day TTL, forcing real-device re-verification.
What the gate does not catch, stated honestly: a wrong spec, behavior nobody wrote a spec entry for, and decomposed-journey gaps. The mitigation for the last is at least one journey-level Maestro e2e per user-facing feature, plus the native artifact layer. See docs/TESTING.md and docs/adr/0001-testing-architecture.md.
| Area | Choice |
|---|---|
| App | Expo (React Native) + Expo Router, TypeScript strict |
| Native modules | iOS Call Directory + Message Filter (Swift), Android CallScreeningService + SMS (Kotlin), via config plugins |
| API | NestJS (TypeScript strict) on AWS Lambda + API Gateway HTTP API |
| Validation | class-validator on every controller DTO; Zod where a schema is shared with the app |
| Auth | JWT access tokens issued by the API, stored on device in expo-secure-store |
| Data | PostgreSQL (Neon serverless) |
| Messaging | AWS SQS (report intake) + dead-letter queue |
| Search | OpenSearch (optional, clustering similar reports) |
| Classifier | LLM endpoint with a deterministic heuristic fallback |
| Push | Expo push (APNs / FCM) |
| IaC | AWS CDK (reusable NestjsApi construct + per-app package) |
| App build/deploy | EAS Build / Submit / Update |
| API deploy | GitHub Actions + CDK over GitHub OIDC (no stored AWS keys) |
| Test runner | @platform/spec-test (Vitest, jest-expo, Maestro, signed artifacts) |
| Tooling | ESLint 9, Prettier, Commitlint (Conventional Commits), Node 20+ |
gh repo create my-app --template elleskay/mobile-platform --clone --private
cd my-app && npm install
cp -r apps/_demo apps/app # overlay native refs from apps/_template
cp -r services/_template services/api
git mv infra/cdk/_template infra/cdk/my-app # edit bin/app.ts stack id
npm run setup # wires GitHub + AWS for the API
# write specs/app.yml, then tests + code until the gate is green, then pushFull step by step: docs/SETUP.md (connection), docs/MOBILE.md (EAS, native), docs/DEPLOY.md (deploy + gotchas).
This is an npm workspaces monorepo (apps/*, services/*, packages/*).
npm ci # install the whole workspace
cd apps/_demo && npm start # expo start (demo app)
cd services/_template && npm run start:dev # local NestJS HTTP on :3000
cd packages/spec-test && npm run build # dual ESM + CJS runnerNative call/SMS features require expo prebuild and a full native build; the references in apps/_template/native/ are copied in via config plugins, not committed as generated ios//android/ dirs. See docs/MOBILE.md.
- Unit and component (app): jest-expo, recorded through
@platform/spec-test/jest. - API: Vitest, recorded through
@platform/spec-test/vitest. - End-to-end (app): Maestro flows, ingested by
spec-maestro. - Native/manual: signed real-device artifacts under
verification/, verified byspec-attestandscripts/verify-attestations.sh. - The gate (
spec-coverage) ties them together and is whatnpm run test:specruns in a cloned app. CI runs the gate against the runner's own samples to prove the pass and fail paths both behave.
Android e2e in CI builds a release APK (not debug), because a debug build loads its JS bundle from a Metro dev server that does not run on a CI emulator. See docs/TESTING.md.
apps/
_template/ Expo app overlay: config plugins, native refs, lib, specs, tests, verification
_demo/ Working demo Expo app (platform self-test)
services/
_template/ Full NestJS service: health, reports, classifier, SQS consumer, OpenSearch
infra/
cdk/_template/ CDK package, includes lib/constructs/NestjsApi.ts
cdk/_setup/ One-time GitHub OIDC + IAM role stack
iam/ Least-privilege deploy policy JSON
packages/
spec-test/ @platform/spec-test: spec runner, coverage gate, ESLint rule, CLIs
scripts/ connect.sh, verify-deploy.sh, verify-attestations.sh
.github/workflows/ ci, security, mobile-build, deploy-api
docs/ SETUP, DEPLOY, MOBILE, TESTING, SSDLC, adr/
MIT. Copyright (c) 2026 elleskay. See LICENSE.