The iOS & Android companion app for SoftTrack — a self-hostable issue tracker.
A mobile client that mirrors the full SoftTrack web feature set — boards, issues, cycles, reports, notifications, search, and administration — talking to the existing FastAPI backend of any SoftTrack instance. Sign-in takes three inputs: the server link (your instance URL), email, and password.
The design mockups and some issue text say "username". The API only ever resolves the OAuth2
usernamefield against the email column (backend/lib_identity/identity.py), so the app asks for an email. Adding a username lookup would be a backend change.
You need a SoftTrack instance to point the app at. The quickest one is the reference stack:
cd ../soft-track && docker compose up --buildThat serves the API on :8000 with a seeded demo account (demo@softtrack.dev / password123).
Then start the app:
npm install && npx expo startOpen it in Expo Go. At the login screen enter your machine's LAN address as the server link — http://192.168.x.x:8000, not localhost, which on a phone means the phone.
src/app/ expo-router routes — thin wrappers, no logic
(auth)/ signed out: login, register
(app)/ signed in: Home, Board, Search, Inbox, You
invite/[token] an invitation — readable either way, so guarded by neither
[teamKey] /ENG — sets the active team, then hands to Board
src/api/ HTTP client, instance URL, generated client
src/auth/ session store, auth context, deep-link capture
src/team/ team context, switcher, creation, invitations
src/board/ board and list views, filters, drag-to-move
src/issues/ priority and status metadata
src/offline/ connectivity, cache persistence, the mutation queue
src/ui/ design tokens, theme, primitives, navigation chrome
openapi/ the vendored API contract codegen reads
src/ mirrors the web app's frontend/src/ naming wherever the concept is the same, so moving between the two repos is uneventful.
src/api/generated/ is Orval output — typed React Query hooks — built from openapi/openapi.json, a vendored copy of the schema the backend produces. Same config as the web app, so hook names match in both clients. Never edit it by hand:
npm run sync:openapi # refresh the vendored schema
npm run generate:api # regenerate the clientCI fails if the committed client does not match the committed schema, and a weekly job flags when the vendored schema falls behind the backend.
Tokens in src/ui/tokens.ts are transcribed from the web's frontend/src/index.css. Note the neutral ramp is inverted in dark mode, so the two palettes are built explicitly rather than derived. The stored key (softtrack.theme) and its contract — absent means "follow the system" — match the web exactly.
npm run lint && npm run typecheck && npm testThe integration suites are opt-in because they need a live server. They drive the real client modules against it — the same code the screens call — and several of them write real rows (fresh accounts and team keys per run):
SOFTTRACK_LIVE_URL=http://localhost:8000 npm run test:livetest:live rather than npm test, because these suites cannot run in
parallel. Registration is throttled per IP and charged even on success, and
several suites register: run concurrently they exhaust the budget immediately,
and the 429 that comes back is an AxiosError whose circular req/res pair
crashes the jest worker trying to serialize it, so the failure surfaces as
Test suite failed to run rather than as the throttle it is. --runInBand
spreads them out. A few full runs in a row will still eventually 429; the
counter lives in the API process, so
docker compose restart backend && docker compose up -d clears it without
touching the database.
They also share one instance, so they pick their fixtures deliberately rather
than by position — see src/api/__tests__/support/team.ts. GET /teams promises
no order, and the team-administration and onboarding suites create teams the API
offers no way to delete, so an instance these have run against accumulates empty
teams permanently. Taking teams[0] eventually lands on one of those.
Production is covered separately by live-production.test.ts, which stays
read-only apart from a single scratch issue it deletes in the same run.
Note experiments.typedRoutes is a dev-time aid: the route union is written by
expo start, not by expo export, so outside the dev server every path
typechecks permissively. Paths built at runtime go through href() in
src/ui/href.ts rather than being cast at each call site.
- Work is tracked on the Mobile App project.
- Design mockups live in
docs/design/mobile/— one SVG per feature issue, each showing the screen at three window size classes:- Phone — compact, <600dp: single pane, bottom navigation
- Foldable (unfolded) — medium, 600–839dp: two panes split at the hinge
- Tablet — expanded, ≥840dp: nav rail with persistent multi-panel layouts
The mockups reuse the web app's design tokens (brand #6342db, tinted neutrals, status/priority colors) from frontend/src/index.css in the main repo, so mobile and web share one visual language.
Every tracked issue is implemented except push notifications, which have no backend to talk to. Sign in to any instance, register, accept invitations, manage teams, work a board, create and edit issues with sub-issues and links, comment in GitHub-flavored markdown, attach photos and files, search, run cycles, read the four reports, save views, administer a team or the whole instance, and keep working with no connection.
Known gaps, tracked rather than hidden:
- Nothing has been seen on a device yet. The suites cover the logic, the mockup geometry and that the trees render, but no one has run this on hardware. The drag gesture in particular is unverified by touch, which is why every move is also reachable by tapping a card. The medium and expanded layouts would need a tablet or an Android emulator to check for real.
- Invitation links cannot be true universal links. A
https://your-instance/invite/…link can only open the app if that exact domain is declared in the build, which is impossible for arbitrary self-hosted hosts.softtrack://invite/<token>works, and falls back to asking you to sign in first, since a custom-scheme link carries no instance. - Push notifications are not built, because the API has no way to register a
device. Issue #10 anticipated this ("requires a backend addition for device
token registration + push delivery"). There is no
/devicesendpoint and nothing APNs- or FCM-shaped anywhere in the schema, so the in-app inbox and the unread badge are what ship; the badge polls once a minute while the app is in front rather than pretending to be pushed. Real push also needs a dev build and signing credentials, neither of which exists yet. - Member counts cost one request per team.
TeamReadcarries no count, so the teams list asks each team for its members. Amember_countfield upstream would remove the fan-out and help the web too. - Offline is verified as logic, not as behaviour. The queue, the conflict rule and the cache are covered by tests, but nobody has watched a phone lose signal, move a card and come back. That needs a device.
- No aurora or glass blur yet. The web's translucent panels are approximated
with opaque surfaces;
backdrop-filterhas no React Native equivalent and per-surface blur is expensive on Android.