SAMPLE AUDIT
Crewdesk is a fictional product. The structure, depth, and judgment calls below are exactly what a real audit delivers — trimmed to excerpt length. A real engagement runs under mutual NDA with a read-only seat on your repo and staging, read-only Stripe, and an hour with your lead dev (access revoked at delivery). On that access, the full audit adds what this excerpt can’t: per-file findings with code citations (one worked example is in §2.2), a complete API-change inventory, and security notes on the token and upload paths. Findings here are illustrative; file paths and code are invented to show the format.
Crewdesk — Mobile-Readiness Audit
Prepared by Micah Hurst · elementalappsolutions.com · July 2026
1. Engagement context
Crewdesk is field-service scheduling for small HVAC, plumbing, and electrical companies — Next.js frontend, Node/Express API, Postgres, Stripe per-seat subscriptions purchased on the web, Intercom, hosted on Vercel and Railway. Roughly $55k MRR, an eight-person team, no native app. Your customers’ field technicians use the mobile-web app on their phones today, and dispatch alerts go out by SMS. You asked three questions: what would a real native app take, can it replace the SMS spend, and does putting an app in the App Store put Apple’s 30% between you and your Stripe revenue.
2. The hard-20% map
Most of a Crewdesk app is ordinary — lists, detail screens, forms. Your own dev, or an AI tool with repo access, can produce that part — so why not just ask Claude with the repo open? Because on each of the five items below, the obvious answer is wrong: the obvious architecture (Capacitor) costs weeks against this codebase, the obvious notification promise (Critical Alerts) won’t be granted, the obvious billing move (the new link-out exemption) is a bet on litigation. What you’re buying is the call, the reason, and the consequence priced in weeks or rejection risk.
2.1 Push notifications — the reason to build this at all
Dispatch alerts are your killer feature, and they currently ride on SMS. The native replacement is APNs on iOS and FCM on Android, with token-based APNs auth (one .p8 key, no expiring certificates). Server side you need a device-token registry keyed to the seat, plus fan-out wherever assignments change — you already have that hook, because it’s where the SMS send lives today. Reuse it.
For v1 I’d put Expo’s push service in front of both APNs and FCM: one integration, per-message receipts (did APNs/FCM accept it, is the token dead — true end-device delivery confirmation doesn’t exist on iOS), and it matches the architecture in section 3. You can drop to direct APNs later if you ever want the third party out; for an eight-person team, that day is not soon.
Mark dispatch changes as Time Sensitive so they break through Focus modes — that’s a standard capability, though Apple checks the use fits. Do not plan on Critical Alerts (the ones that override the mute switch): Apple grants that entitlement for health, safety, and security apps, and a scheduling app won’t get it. I’m telling you now so nobody promises it to a customer.
What replaces the SMS spend — honestly: not all of it, and not on day one. Push is best-effort delivery, not guaranteed. Techs decline the permission prompt; phones die in crawlspaces. The right v1 is push-first with SMS fallback — if a dispatch isn’t acknowledged in-app within a few minutes, or the seat has no registered device, the existing SMS path fires. Your SMS bill declines with adoption instead of a flag-day cutover that strands whoever didn’t install the app. The economics still work: SMS runs roughly a cent a segment plus A2P 10DLC registration and carrier filtering headaches; APNs and FCM cost nothing per message, and a push deep-links straight to the job screen, which SMS never did well.
Why mobile web can’t carry this: iOS web push exists (since 16.4), but only after the user manually installs your site to their home screen. iOS 26 made home-screen sites open as web apps by default, which helps, but there’s still no install-prompt API on iOS — you’d be betting your killer feature on every technician performing an install ritual nobody performs.
2.2 Auth handoff — your web sessions → native token lifecycle
Today the Next.js layer issues an HTTP-only session cookie and the Express API trusts it. Don’t try to bridge that cookie into a native app through a WebView — it’s fragile and it inherits web session lengths, which are wrong for field techs. The native app should authenticate with tokens: a short-lived access token plus a rotating refresh token bound to a device record, stored in the iOS Keychain (Android Keystore equivalent).
Your per-seat billing makes revocation a business requirement, not hygiene: when a customer deactivates a tech’s seat, that device’s refresh token has to die the same day, or you’re giving away seats. That’s a device table and a revocation check, designed in from the start.
A worked finding, in the format real audits use (paths and code invented for this sample): the Express auth middleware at api/src/middleware/auth.ts:114 verifies the session and attaches the user without consulting seat status:
const session = await verifySession(req.cookies.sid);
req.user = session.user; // seat.status never checked
Harmless on the web, where deactivation kills the cookie. Carried into the token flow, it becomes the seat leak above: a deactivated tech’s refresh token keeps minting access tokens. The fix is two lines — load the seat with the session, reject on status !== 'active' — plus the revocation check in the refresh endpoint. A real audit has a dozen findings at this grain.
Set refresh lifetime long — 60 to 90 days — so techs aren’t re-entering passwords in a truck. API work is contained: token issue/refresh/revoke endpoints plus Bearer support in the existing auth middleware. Call it a week including tests — assuming auth stays the single Express middleware layer it is today; that’s the assumption a real audit verifies before the estimate ships.
2.3 Offline at the job site — what to build and what not to promise
Mechanical rooms, basements, and rural routes are dead zones, and that’s exactly where your users work. This is the one your mobile-web app genuinely cannot fix: iOS Safari has no Background Sync API at all — no timeline for one either — and browser storage is evictable, so a status update composed without signal is lost work waiting to happen.
Native v1, scoped honestly:
- Read side: cache today’s assigned jobs in local SQLite, refreshed on morning sync and on each push.
- Write side: an outbound queue for status changes, notes, and photos — idempotency keys, retry with backoff, server timestamps. Keep writes append-shaped (status events, notes, attachments) and conflicts mostly disappear by design, because at most one tech and one dispatcher touch a job.
What v1 should not promise: offline-first everything. No offline job creation, no offline edits to customer records, no multi-day offline schedule sync. The bar is “a tech in a basement can finish the job screen and trust it uploads later” — offline-tolerant, not offline-first. The difference is several weeks of sync-engine work you don’t need yet, and it’s the most common way this kind of project silently doubles.
2.4 Photos — capture is easy; surviving the upload is the work
Job-documentation photos mostly work in mobile Safari today — the camera comes up, the picture takes. The failure is what happens next: the tab suspends or the signal drops mid-upload and the photo is gone — the pattern behind the “the photos didn’t attach” support tickets an audit typically finds in the queue. Native plan: compress on device (documentation doesn’t need 24-megapixel HEIC originals — a 2048px JPEG around 300KB is plenty), upload direct to object storage with presigned URLs instead of streaming through the Express box on Railway, ride the same offline queue as 2.3, and use iOS background transfer so pocketing the phone doesn’t kill an in-flight upload. Presigned URLs are about a day of API work; everything else is shared with the offline queue.
2.5 Stripe vs Apple — the 30% question, precisely
The answer for your model: Apple takes nothing — if the app is built the way this document scopes it. Here’s the exact reasoning, because you should be able to check it.
Apple’s in-app purchase requirement (Guideline 3.1.1) applies to digital goods and services sold inside the app. Crewdesk sells nothing inside the app: subscriptions are per-seat, bought on the web by the company, and technicians are provisioned users who never see a price. Guideline 3.1.3(f) — Free Stand-alone Apps — covers this exactly: a free app acting as a companion to a paid web-based tool doesn’t need IAP, provided there’s no purchasing inside the app and no calls to action to purchase outside it. Guideline 3.1.3(c) (Enterprise Services, software sold to organizations) points the same direction.
What that proviso means in practice: no “upgrade your plan,” no “buy more seats,” no pricing page, no billing links anywhere in the app. When a deactivated seat logs in, they see “ask your account admin” — a dead end, not a link. That one screen is essentially your entire compliance surface.
The 2026 nuance a careful reader will want: since the Epic contempt ruling in April 2025, apps on the US storefront may link out to external purchase flows, and Apple is currently collecting no commission on those link-outs. The Ninth Circuit affirmed the contempt finding in December 2025 but sent the remedy back down — and it has already said Apple may charge a cost-based commission on link-outs once the district court sets a rate. The Supreme Court took Apple’s appeal at the end of June 2026. So yes — you could legally put a billing link in the US app today, commission-free. My call: don’t. That zero-fee window can close on remand regardless of how the Supreme Court case goes, and it’s US-only — while the purchase-free posture under 3.1.3(f) is stable, applies on every storefront, and costs this product nothing; nobody buys HVAC dispatch software from a technician’s phone. If you ever add a self-serve tier sold in-app, that transaction takes 30% (15% under Apple’s Small Business Program, below $1M/year in post-commission App Store proceeds). Not a v1 question.
3. Architecture: React Native with Expo — and what not to build
I weighed four options against this team (eight people, all TypeScript, zero native experience) and the actual hard requirements (push, an offline queue, reliable background photo upload).
Capacitor looks like the cheap path and I want to be straight that I do recommend it for some products — client-rendered SPAs wrap well. It’s wrong here for a concrete reason: Capacitor ships static files into a WebView with no Node at runtime, and your Next.js app leans on server rendering. So “reuse the web app” really means either re-architecting the web product to a static export — not cheap, and it changes the thing that’s earning $55k MRR — or pointing a shell at the live site, which gives you no offline capability and is the canonical Guideline 4.2 “minimum functionality” rejection. The cheap path isn’t cheap for this codebase.
Native Swift plus Kotlin is the best per-platform result and the wrong team fit. Your techs carry whatever phone they own — a big share Android — so it’s two codebases in languages nobody on staff writes. You’d be renting maintenance from someone like me forever. I decline that on your behalf.
Hotwire Native is built for server-rendered HTML apps (Rails-shaped); it doesn’t fit a React codebase and inherits the WebView’s offline limits anyway.
React Native, using Expo is the call. It’s the same language as the rest of your stack; the old bridge architecture that fed RN’s performance reputation is gone (the New Architecture has been mandatory since 2025); Expo is the framework the React Native team itself recommends; and EAS handles signing, provisioning, and builds — worth real money to a team with no Xcode muscle memory. Mature modules exist for everything in section 2. Most importantly, your team can own it after I leave. That criterion alone eliminated Swift.
v1 is a technician app, full stop: today’s jobs, job detail, status flow, notes, photos, push. Deliberately not in v1: the dispatcher scheduling board, reporting, invoicing, customer management, seat and billing admin, iPad layouts. The dispatch console lives on a desk; leave it on the web. And one thing not to rebuild at all: job-history and customer-record browsing already work fine in your mobile-web app whenever there’s signal — v1 should deep-link to those pages, not reimplement them. Rebuilding screens that already work is how $20k apps become $60k apps.
4. App Store readiness
What would trip review for this app, specifically:
- Login-required app (Guideline 2.1). App Review must be able to use the app, so you provide a demo account in the review notes: a seeded fake company, a realistic day of jobs, and a way to see a dispatch push land (a trigger inside the demo org is the clean answer). Expired demo credentials are a classic rejection on later updates — keep the demo org alive as a release-checklist item.
- Minimum functionality (4.2). Kills thin web wrappers. The React Native plan clears it inherently; the remote-URL shell from section 3 wouldn’t.
- Account deletion (5.1.1(v)). Only required when the app supports account creation. V1 has login, no signup — seats are admin-provisioned — so it doesn’t strictly apply. Cheap insurance anyway: a settings link to the web account page and a sentence in the review notes.
- Sign in with Apple (4.8). Only triggered by offering third-party or social login. Email/password only means it’s not required. If you ever add “Sign in with Google” to the app, Sign in with Apple (or an equivalent privacy-preserving option) comes with it — decide then.
- Push rules (4.5.4). The app must work with the permission denied (it will — the job list doesn’t depend on it); no marketing pushes without explicit opt-in. Dispatch alerts are transactional and fine.
- Privacy plumbing. Privacy labels, purpose strings, privacy manifest — mechanical; each is a rejection if skipped.
- Developer account. Enroll as an organization in week one: $99/year, D-U-N-S number, entity verification, days to weeks of lead time. EAS handles certificates; TestFlight covers internal testers and a two-to-three-customer external pilot; review usually turns in 48 hours — budget one rejection round-trip anyway.
5. Effort map
| Phase | What ships | Effort |
|---|---|---|
| 0 — Foundations | Expo project, EAS pipeline, token auth (API + app), navigation skeleton; developer-account enrollment starts | 1.5–2 wks |
| 1 — Technician core | Today’s jobs, job detail, status flow, notes | 2–2.5 wks |
| 2 — Push | Device registry, dispatch fan-out, Time Sensitive alerts, SMS fallback tiering | 1–1.5 wks |
| 3 — Offline + photos | SQLite cache, outbound queue, presigned uploads, background transfer | 2–3 wks |
| 4 — Ship | Hardening, TestFlight pilot with 2–3 real customers, store assets, review | 1–1.5 wks |
Roughly 8–10 weeks elapsed. Phases 1–2 produce a pilotable app; phase 3 is what makes it trustworthy in a basement. This plan is executable by me, by a hire (it doubles as the technical half of the job spec), or by any competent shop.
Appendix — the quote, and the conflict of interest in it. Yes: the person who scoped this work is also bidding on it. Here’s what keeps that honest. Fixed price to build phases 0–4 as scoped: $19,500 — 50% to start, 50% on delivery. The audit terms carry over, with an objective test instead of my say-so: send this plan to any competent shop; if they can’t produce a fixed bid from it, the second half of the audit fee is waived. The full $3,500 audit fee is credited against the build if you decide to build with me within 90 days — one credit per client. Whichever way you go, the plan is yours.
— Micah Hurst · 22 years shipping software, iOS since 2010 · elementalappsolutions.com