From Round-2 complete to App Store launch
Round 2 is shipped — eight clean commits in v5. Next: build the multi-tenant foundation, harden for production, and prepare both stores. No deadline, no shortcuts — every step taken now saves a multiple of itself later.
Pty Ltd registration → Apple Developer Program enrolment ($A150/year). Apple's review of new dev accounts is currently 2–6 weeks. Use that time productively (Phases 1–4 below).
Recommended sequencing
-
Multi-tenant refactor
Restructure Firestore so each construction company is a top-level Organisation. Projects and action items become subcollections. Per-org roles, per-org membership. Done before real customer data exists — migrating later is dramatically more work.
-
Production hardening
Sentry error monitoring. Account deletion flow (Apple requirement). Tightened Firestore security rules. Photo upload retry/resume for site network conditions. Network resilience audit.
-
Assets & polish
Final logo (pending colour decision). Icon generation at every required size. Splash screens. App Store and Play Store screenshots designed in Figma. Listing copy written.
-
Automated testing
Jest unit tests on critical utilities (item status, date logic, role checks). Detox E2E smoke test on the core happy path. CI on every PR. Minimum coverage on the parts that break silently.
-
TestFlight rollout
After Apple Dev account lands. Build via EAS, upload to App Store Connect, internal testing track, invite friends across the device matrix. Iterate on real-device feedback.
-
App Store + Play Store submission
Submit to both stores in parallel. App Privacy questionnaire, Data Safety form, content ratings, screenshots, listing metadata. Respond to first-pass rejections.
-
Billing & subscriptions
Stripe per-seat subscription model. Each company pays per active user (1 manager + N foremen). Trial period. Webhook handling for lifecycle events. Receipt validation.
Tenant model — the architecture
Build Pro HQ sells to construction companies. Each company is an Organisation with one manager (admin) and N foremen (regular users). The manager creates projects, assigns foremen to projects at their discretion, and manages the seat count via the Build Pro HQ subscription.
Build Pro HQ itself manages many Organisations. v1 must comfortably handle at least 5 Organisations with multiple users each.
Key design choices
-
1Roles are per-organisation, not globalSebastian might be a Manager in Org A and a User in Org B (e.g. as a consultant). Role lives on the
memberssubcollection, not on the user document. -
2Org membership is the security boundaryFirestore rules check
request.auth.uid in get(orgDoc).data.memberUidsfor any access underorganizations/{orgId}/**. One company cannot see another's data, period. -
3Seat-based billing — Stripe handles the countManager subscribes to N seats via Stripe.
seatLimiton the org enforces it client-side; Cloud Function syncs from Stripe webhook. Adding a foreman beyondseatLimitprompts a seat upgrade. -
4Org switcher UI for users in multiple orgsMost users will only ever be in one org. For those who aren't, the dashboard top bar gets a small org switcher. Mirrors the project switcher pattern that's already built.
Multi-tenant refactor
Restructure Firestore so every construction company is an isolated tenant with its own projects, members, and roles. The most consequential single piece of work before launch — and easiest to do now while there's no real customer data.
- Fields:
id, name, ownerUid, memberUids[], seatLimit, subscriptionStatus, stripeCustomerId, stripeSubscriptionId, createdAt. - Fields:
uid, role: 'manager' | 'user', addedAt, addedBy. Lives atorganizations/{orgId}/members/{uid}. - Role moves to per-org. Remove
rolefield from the user document. - Replace the existing flat data model with the nested organisations/projects/actionItems hierarchy. Include the diagram.
- Migrate the existing single-tenant project doc into a "default" organization for dev data continuity.
- Subcollection. Path becomes
organizations/{orgId}/projects/{projectId}. - Path:
organizations/{orgId}/projects/{projectId}/actionItems/{itemId}. DroporgIdandprojectIdfields from the document — implied by path. - Seed 2 example organisations with 2 projects each and a handful of action items per project. Useful for testing the org switcher UI.
- Modify
firestore.indexes.json— collection group queries onactionItemsmay need rebuilding.
- Mirror
ProjectContext's pattern. Persists last-selected org to AsyncStorage. - Provider order matters — Org must resolve before Project queries can scope correctly.
fetchActionItems,createActionItem,updateActionItem,fetchProjects,createProject, etc.- Decision pending client feedback. Architecture supports it (
organizationIds[]on user,currentOrgIdon user doc), but in v1 the UI shows the user's single org with no switcher. If multi-org is locked in later, this task ships in v1.1. - Member list (replaces existing Users section) with role + Remove button. "Invite foreman" button → opens InviteForemanModal. Seat usage indicator. Existing Projects/Trades/Levels stay below as per-project.
- Role pill reflects
currentMemberRolefromOrganizationContext, not a global user role. Format: "Acme Construction Pty Ltd · Manager".
firebase init functions. Functions live infunctions/src/index.ts.- Validates manager role + seat capacity. Creates
invites/{inviteId}doc with 7-day expiry. Triggers email send. - Renders HTML email with deep link
build-pro-hq-v5://invite/{inviteId}. Uses Firebase Trigger Email Extension (or direct SendGrid/Postmark SDK). - Validates not expired, not consumed, caller email matches invite. Adds caller to org's
members+memberUids. Marks invite consumed. - Email input → calls
createInvite. Success/error states. Disabled if at seat limit. - Handles
build-pro-hq-v5://invite/{inviteId}. If logged out → routes to accept-invite signup. If logged in → callsacceptInvitedirectly. - Email pre-filled from invite, password input. Creates Firebase Auth user, calls
acceptInvite, routes to dashboard. - For randoms who download from App Store without an invite. Headline: "Build Pro HQ is for construction companies." Body: explain web-first signup. Buttons: "Open buildprohq.com.au" (uses expo-web-browser) + "I have an account". Required for App Store Guideline 2.1 compliance.
- Edge case: foreman lost the email or got the code via SMS. Single text input, "Continue" → routes to invite handler.
- Existing login form + new "Get started" CTA + "Have an invite code?" small link below. No pricing language anywhere (anti-steering).
scheme: "build-pro-hq-v5"already set; verifylinkingconfig in expo-router handles/invite/[inviteId]route.- New subsection in Section 12 noting
review@buildprohq.com.auaccount must exist in production Firebase before submission. Actual seeding happens in Phase 3/5 — this is just the documentation reminder.
- Helper functions
isOrgMember(orgId)andisManager(orgId). Members subcollection writes blocked client-side (Cloud Functions only). Org-doc writes restricted tonameby manager. allow get: if true(the inviteId is the secret),allow list: if false,allow write: if false.firebase init emulators. Pick Firestore + Functions + Auth.- User from Org A cannot read or write anything in Org B. Use
@firebase/rules-unit-testing. - Foreman attempts must be rejected. Manager attempts must succeed.
- Manager attempting to update
seatLimitorsubscriptionStatusmust be rejected (only Cloud Functions write these via Admin SDK). firebase deploy --only firestore:rules --project build-pro-hq-v5---dev-only.- Manually create a second org via Firebase Console with a different test user. Log in as each user, confirm they only see their own org's data.
Production hardening
Everything that separates "works on my simulator" from "ready to take real customer money." Error monitoring, account deletion (Apple-required), tightened security, network resilience for site conditions.
- Free tier handles ~5,000 errors/month. Plenty for early-stage.
npx expo install @sentry/react-native. Configure inapp.jsonvia the Expo plugin.- Wrap
RootLayoutinSentry.wrap(). SettracesSampleRate: 0.2for performance monitoring. - Falls back to a "Something went wrong — please restart" screen. Reports the error to Sentry.
- Throws a test error so you can verify the Sentry pipeline is wired up.
Sentry.setUser({ id: uid, email, orgId: currentOrgId })— makes triaging much easier.
- Below the Log Out button, in a "Danger zone" section with red border. Subtitle: "This permanently deletes your account and removes you from all organisations."
- Standard pattern — user must type "delete" exactly to enable the delete button. Prevents accidental tap.
- Block with "Transfer ownership or delete the organisation first." Provides a path: an "Transfer to..." picker.
- Removes user from all orgs'
memberUids, deletes their assigned action items (or reassigns to manager), deletes their user doc. Then callsauth.deleteUser(uid). - Show a brief "Account deleted" toast on the login screen.
- Create test user, populate data, delete, verify nothing remains. Required evidence for App Store review.
- Resize to max 2048px on the long edge, JPEG quality 0.8. Reduces upload time and storage cost dramatically.
expo-image-manipulatorstrips EXIF by default in re-encoding. Document this for the privacy questionnaire.- 3 retries with 1s, 4s, 16s delays. Don't retry on 4xx errors (client mistake won't get better).
- Queue:
{ itemId, localUri, attempts }[]. Survives app force-quit. Resume on next launch. - Subtle progress dot or spinner while photos are uploading. Failed indicator if all retries exhausted.
- Manual QA: start upload, toggle airplane mode on, watch retry, toggle off, confirm upload completes.
- Complete/reopen already does this. Verify create, edit, delete, photo, drag-reorder all follow the pattern.
- Use
@react-native-community/netinfo. Show a non-intrusive banner: "Offline — changes will sync when you reconnect." enableIndexedDbPersistence(db). Caches reads, queues writes, automatic. Works out of the box on RN.- Open app, toggle airplane mode, navigate around, create items. Should feel functional. Toggle off, watch sync.
- Cross-reference: this is the Phase 1 rules work. Confirmed deployed before any submission.
- iOS 17+ requirement. Expo's plugins handle most of it; verify Firebase's privacy manifest is included.
- Be specific in the strings — generic permission text is a frequent App Store rejection.
- Production logs leak through to native console / Sentry breadcrumbs. No PII in production logs.
- EAS production profile should set this. Verify via the EAS Build config.
- v5 already does this for most screens. Verify Account, Management/Organisation, project list.
- Skeletons feel faster.
SkeletonRowis already incomponents/; verify usage everywhere. - No "fatal" dead-ends. The existing error banner pattern (tap to retry) should be everywhere.
Assets & polish
App icons, splash screens, App Store screenshots, listing copy. The visual layer between your code and the customer's first impression.
Final logo + colour decision. Most of this phase can start once that's locked in.
- Apple applies the rounded-rectangle mask. Ship a square PNG with the design extending to the edge.
- Set
iconinapp.json→ Expo handles the matrix. Verify withnpx expo prebuild. - Foreground = logo on transparent. Background = solid colour or gradient. Already configured in
app.json; replace the placeholder PNG. - Detail loss at small sizes is the most common icon problem. View on a real Home Screen, not just in design tools.
- Already configured in
app.json. Replace placeholder splash icon when logo finalised. - Set
userInterfaceStyle: "dark"(already done) prevents the iOS auto-light flash on launch.
- Each with a bold heading: "Track every action item," "Share to your team in seconds," "Manager view across the whole crew," etc. Headings convert; raw screenshots don't.
- Required minimum. Use iPhone 15 Pro Max simulator or a real device.
- Required for any app marked as iPad-compatible. Show landscape on at least one.
- Apple stopped requiring 6.5" but accepting it; fine to skip.
- 2–8 images. Most apps use 1080×1920. Same Figma templates as App Store work fine.
- If you support tablets (you do, supportsTablet=true), Google requires tablet shots for the listing.
- Same.
- Hero image at the top of the listing. Brand-forward.
- "Build Pro HQ" — 12 chars, fits.
- e.g. "Site action items for trades" (29 chars). Tells the buyer who it's for.
- Updatable without a new build. Use for launches, sales, news.
- Lead with the problem (chasing site defects across multiple foremen). Then the solution. Then features as a list. Close with social proof if available.
- "construction, builder, tradie, action items, site management, defects, snag list, punch list, project management"
- Different from App Store subtitle — Google's appears below the icon in search results.
- Share icon, search icon, account icon, multi-select, etc. Required for VoiceOver. Apple sometimes rejects for missing accessibility.
- Use contrast-ratio.com or Stark in Figma.
colors.textMutedoncolors.bgis borderline — verify. - Triple-click home button (or settings) to enable. Walk through every flow.
- Settings → Accessibility → Display & Text Size → Larger Text. Watch for clipped labels.
Automated testing
Right-sized for where the project is. Critical-path coverage that catches catastrophic regressions, not exhaustive component tests that you'll never maintain.
npx expo install jest jest-expo @types/jest. Configurejest.config.jswithpreset: "jest-expo".- Open vs overdue vs due-today vs completed × with/without dueDate. ~12 cases. Fastest way to catch a date-math regression.
- Verify
isTablet()returns correctly for iPhone, iPad, Android phone (small), Android tablet (large). - Extract the format function to
utils/shareFormatter.tsfirst. Test every branch: with/without notes, photos, due date. - Adding a foreman beyond
seatLimitmust throw / be rejected.
npm i -D detox. Config in.detoxrc.js. Build for testing with EAS.- Catches catastrophic regression. Add more later, only when motivated.
- Don't pollute dev Firestore with test data. Emulator resets cleanly each run.
- Currently 8 commits sitting locally. Set up GitHub repo, push.
- ~30 lines of YAML. Catches type breaks immediately.
- macOS minutes are expensive on GitHub Actions free tier. Consider running only on PRs to
main.
TestFlight rollout
After Apple Dev account lands. Real-device testing across the matrix, friends and colleagues, iterate.
Apple Developer Program enrolment. Cannot start TestFlight without it.
- Bundle ID, primary language, app name, default platform.
- Production bundle ID should drop the version. Suggest
au.com.buildprohq.app(Australian convention). - Required for EAS to upload builds. Settings → Users and Access → Keys → App Store Connect API.
eas credentials. Stores the key encrypted in EAS so future builds upload automatically.
eas build --platform ios --profile production. Takes 15–30 min on EAS cloud.eas submit --platform ios. Apple processes it (~30 min).- No review required. Up to 100 internal testers.
- Apple reviews the build first (~24 hours). Then up to 10,000 external testers.
- Aim for at least one tester on each Tier 1 device: iPhone 14/15, iPhone SE, iPad Pro 11", iPad 9th gen.
- Tag by device + severity. Triage daily during the test period.
- First TestFlight always surfaces 5–10 issues. Don't submit on TestFlight Build #1.
App Store submission
Apple-specific submission checklist. Privacy questionnaire, age rating, export compliance, screenshots, listing fields. Most rejections happen on first submission — plan to iterate.
- e.g.
https://buildprohq.com.au/privacy. Required. - e.g.
https://buildprohq.com.au/supportor a contact form. Required. - Marketing site homepage.
- High effort, marginal conversion. Add in a later release.
- Firebase Auth.
- Display name from Firestore.
- If you set
Sentry.setUserwith email, this becomes "linked to identity" — be precise. - Means no IDFA prompt needed. Verify Firebase Analytics is configured to NOT collect IDFA.
- B2B construction app, no concerning content.
ITSAppUsesNonExemptEncryption: falsealready set inapp.json. Verify.
- Dedicated reviewer account. Don't reuse a real customer email. Strong password stored in your password manager.
- In production Firebase. Set
seatLimit: 99manually so the reviewer never hits a limit during exploration. - Variety: open / completed / overdue / due-today. Some with photo attachments, some with notes. Mix of trades and levels. Demonstrates the app's range to a reviewer in 60 seconds.
- Manager role lets the reviewer see the full app surface (Management screen, member list, draggable levels, etc.).
- Called "Test Org B" with 1 project and a few action items. Reviewer never logs into it but you reference it in App Review notes to prove isolation.
- Test on a fresh simulator with the production build. Apple will log in. If it fails, instant rejection.
- Reviewer might tap "Get started" instead of logging in with provided credentials. The explainer screen + "Open buildprohq.com.au" must work.
- Apple checks for it specifically. Frequent first-submit rejection cause.
- Common rejection. Production listing must look production.
- Suggested text: "B2B SaaS for construction companies. Real customers sign up at buildprohq.com.au. Test credentials provided. App is multi-tenant — Test Org B exists separately to demonstrate isolation. Tap 'Get started' on the login screen to see the explainer for new users."
- Email:
review@buildprohq.com.au. Password: from password manager. Apple stores these encrypted.
- Review takes 24–48h typically.
- ~30% first-submission rejection rate. Plan for 1–2 cycles.
- Approved doesn't mean live. "Manual" gives you control over the launch moment.
Play Store submission
Google Play Console submission. Generally faster review than Apple but with a 14-day closed-test requirement before production for new developer accounts.
- Verification can take a few days. Identity verification + business verification.
- Service account JSON from Google Cloud → Play Console linkage.
eas build --platform android --profile production. Outputs an AAB.
- Same
review@buildprohq.com.auaccount created for App Store. Google asks for these in App access → "All or some functionality is restricted." Tick that box. - Google reviewers are less rigorous than Apple's but read it anyway. Saves you a back-and-forth.
- Auto-rates across regional systems. Honest answers — they're trivially easy to verify.
- Same data, different form. Be consistent across the two stores.
- Google now requires this before allowing graduation to production for new dev accounts. Plan around it.
Billing & subscriptions
Per-seat Stripe subscription, one Stripe Customer per construction company. Stripe holds the source of truth for seat count and status; the app reads via webhook-synced fields on the org doc and enforces accordingly. Australia-first launch — GST handled via Stripe Tax.
You have one Stripe account. Each construction company is a Stripe Customer with one Subscription. quantity on that subscription = seat count. Cloud Functions sync subscription state to organizations/{orgId} via webhooks. The app reads seatLimit + subscriptionStatus from the org doc; client never writes those fields.
This pattern (web billing, app shows status) is explicitly allowed under "Multiplatform Services." It's how every B2B SaaS app ships (Slack, Asana, Notion, etc.). No Apple IAP required, no 15–30% cut. Australia-first launch means the EU DMA rules don't apply to you.
A. Architecture & pricing decisions
- Recommended: $A19–29/user/month. Survey 2–3 prospective tradies before locking it in. Mid-market construction SaaS sits around $A25/seat.
- Recommendation: included free. Reduces buyer friction ("1 manager + 5 foremen = pay for 5"). Easier to message.
- e.g. monthly $A25/seat → annual $A250/seat (saves ~$50). Annual buyers churn far less, prepay smooths cash flow.
- Lower-friction signup. If "tire-kickers" become a problem post-launch, switch to "card up front, charge after 14 days."
- Recommendation: web-first. Manager signs up at
buildprohq.com.au/signupon desktop, enters card, then downloads the app. App users (foremen) come in via invite link, never touch billing.
B. Stripe account & product setup
- Account name: "Build Pro HQ Pty Ltd" once incorporated. Use a generic email until then.
- Required to activate live mode. Stripe needs identity verification on Pty Ltd directors.
- Settings → Tax. Auto-calculates 10% GST for Australian customers. Costs 0.5% of taxed transactions. Worth it from day one — no manual GST headaches at EOFY.
- Mandatory above $75k. Even below, voluntary registration lets you claim GST credits on business expenses. Talk to your accountant.
- Description: "Per-foreman monthly subscription to Build Pro HQ, including the manager seat at no extra cost."
- Quantity-based is simpler — pass
quantity: seatCounton the subscription. Stripe multiplies. Per-seat invoicing automatic. - Settings → Billing → Customer Portal. Toggle features on. Pre-built hosted page; saves weeks of work building it yourself.
- Or "Tax-inclusive" if you prefer to advertise GST-inclusive prices. Talk to your accountant — affects how prices are displayed in marketing.
- Settings → Branding. Stripe sends professional-looking invoices automatically.
- Settings → Billing → Subscriptions and emails → Smart Retries. ML-driven retry timing. On by default; verify settings.
C. Firestore data model & security
stripeCustomerId, stripeSubscriptionId, subscriptionStatus, seatLimit, trialEndsAt, currentPeriodEndsAt, billingInterval. See data model on Overview.- Manager can update
name,memberUidsvia specific rules. Billing fields only writable by Cloud Functions (which use the Admin SDK and bypass rules). - Each webhook event ID gets stored after successful processing. Replays are detected and skipped.
D. Cloud Functions backend
firebase init functions. Use TS to match the rest of the codebase.cd functions && npm i stripe. Pin to a known-good version.STRIPE_SECRET_KEY(live and test variants),STRIPE_WEBHOOK_SECRET. Never put these in code or env files.
- Called when a new manager signs up. Creates Stripe Customer (with metadata:
bphqOrgId,bphqOrgName,bphqManagerEmail), creates Subscription withtrial_period_days: 14andquantity: 1, writes the neworganizations/{orgId}doc with the resulting IDs. - Called from the app's "Manage subscription" button. Verifies the caller is the org manager, then creates a Stripe Billing Portal session and returns the URL. App opens it in a webview / external browser.
- If the deleted user is sole manager of any org with active subscription, block. Otherwise: remove from
memberUids, decrement Stripe quantity if seat is freed (or leave for manual reduction).
- Use
onRequest. Stripe sends events as POST requests; respond 200 fast. stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET). Without this, anyone who knows your endpoint can spoof events.- Stripe retries webhooks; same event can arrive twice. Store event ID after success, return 200 immediately on duplicate.
- Look up org by
stripeCustomerId(or fall back tometadata.bphqOrgId). SyncseatLimit,subscriptionStatus,currentPeriodEndsAt,trialEndsAt. - Set
subscriptionStatus: 'canceled'. App's read-only mode kicks in. - Confirm or flip
subscriptionStatus. Stripe handles retries automatically; your job is just to mirror state. - Fires 3 days before trial ends. Trigger an email to the manager: "Your trial ends in 3 days — add a payment method to continue."
- Developers → Webhooks → Add endpoint. Stripe gives you the signing secret to store in step 23.
onSchedule('0 3 * * *'). Lists all active Stripe subscriptions, comparesquantity+statusto each org doc, logs drift to Sentry, optionally auto-corrects.- Find orgs with
subscriptionStatus === 'past_due'for 7+ days. Mark them for soft-lock, email the manager.
E. App integration
- Above the Log Out button. Shown only when the user is the org manager. Hidden for foremen.
- Example layout: "Foreman Seats × 5 · Active · Renews 14 May." Status pill colour from existing palette.
- Calls
createCustomerPortalSessionCloud Function. Opens result URL inWebBrowser.openBrowserAsync()fromexpo-web-browser— uses Safari View Controller / Custom Tabs, not webview (App Store prefers this). - "Trial ends in 2 days — add a payment method to continue" with the Manage subscription button.
- Persistent banner. Manager-only "Update payment" button to portal.
- Cross-reference with Phase 1 invite flow. Modal: "You're at your seat limit (5/5). Upgrade your subscription to add another foreman." → portal.
- Disable creating action items, photos, project changes. Existing data still readable. Banner: "Your subscription is paused — update payment to resume."
- After 30 days Stripe will have given up retries. Treat as essentially canceled; don't delete data, just freeze access.
F. Apple App Store compliance
- No "$A19/seat/month" anywhere in the app. Pricing lives only on the marketing site and Stripe Customer Portal.
- Plain "Manage subscription" button is fine. "Upgrade — only $A29/month!" is anti-steering.
- "You're at your seat limit. Manage your subscription." — fine. "Cheaper if you upgrade on our website" — not fine.
- When submitting: "This is a B2B SaaS app. Subscriptions are purchased on buildprohq.com.au by the construction company manager. App users (foremen) access content their company has acquired via Multiplatform Services (3.1.3(b))."
G. Operational workflow — what Sebastian does day-to-day
- Shows MRR, active subscriptions, churn, failed payments. Glance once a day. No external dashboard tool needed for the first 50–100 customers.
- Stripe Dashboard → Settings → Email preferences. Tier them by importance (cancellations = immediate; new subscriptions = daily digest).
- If the daily reconciliation Cloud Function logs drift to Sentry, you want to know.
- Stripe Dashboard → find Customer (search by email or
bphqOrgIdin metadata) → click invoice → "Refund." Two clicks. - Customer Portal allows managers to do it. If you want to comp manually: Stripe Dashboard → subscription → "Update quantity" with proration off.
- Subscription → "Update" → "Extend trial" by N days. Webhook fires automatically; org doc syncs.
- When you eventually onboard a support hire, "Billing" label means "needs Stripe Dashboard access."
- CSV export of all transactions. Hand to accountant for BAS / EOFY.
- Stripe has native Xero integration. Auto-syncs invoices and payouts. Saves your accountant a lot of time once you have >20 customers.
H. Testing the lifecycle
- Use the same name/price. Note new live Price IDs and update Cloud Functions config.
Accounts & services to set up
Every external account/service needed to operate Build Pro HQ as a real product. Order matters — some block others.
- Hosts privacy policy, support page, terms, marketing pitch. Cloudflare Pages or Webflow.
- Required for both stores. Hand-write or use Termly. Drafted Phase 3.
- Especially important for B2B SaaS — defines the contract.
- Required for store listings. Forward to your personal email until you have a help desk tool.
- Don't share dev and production. Set up
build-pro-hq-prodwith the same config. Switch via env var.
- Australian company enrolment requires DUNS number, ABN. Allow 2–6 weeks for approval.
- Identity verification typically 1–7 days.
- Required to receive any revenue. Multi-step. Tax form is W-8BEN-E for Australian companies selling in the US.
- When you have ~10+ customers. Until then, support@ + a Trello board is fine.
- Enterprise hygiene. Optional for v1.
- Firebase Analytics is fine until you outgrow it. Defer.
- Only worth it if you go App Store-billed subscriptions. Not needed for Stripe-only model.
Device test matrix
Tier 1 = must work flawlessly. Tier 2 = should work, edge cases tolerable. Use during TestFlight + Play Store internal testing.
- Most common phone among prospective users.
- Often forgotten; common with budget-conscious tradies.
- Cheap and effective. Aim for at least one tester per Tier 1 device.
- One month before submission lets you cover devices nobody you know has.
- Caveat: emulators don't replicate real network conditions, photo capture, or haptics. Real devices are still required.