8.9 KiB
Warmbly iOS — local development guide
This is the end-to-end guide for running the iOS app against a local backend. If you already have the native dev stack running, skip to First run.
Prerequisites
- Xcode 26 or newer. The project targets iOS 26 and uses the folder-synchronized project format, so any
.swiftfile added underios/Warmbly/is compiled automatically. You never edit the.pbxprojto add files. - The repo's dev stack running natively (below). The Go services and frontends run on the host; only the backing infrastructure runs in Docker.
Step 1: start the backend stack
From the repo root, in separate terminals (leave them running):
make infra # postgres, redis, kafka, mailpit, localstack, etc. in Docker. Run once.
make backend # the API on :8080 (applies migrations on boot)
make realtime # the Phoenix websocket service on :4000 (live updates + presence)
make seed # one-shot: loads fixtures and dev users. Safe to re-run.
make realtime is optional but recommended: without it the app still works, but lists won't update live and presence is empty. It needs the Elixir toolchain on the host. make run is a shortcut that runs backend + consumer + worker in one terminal if you'd rather not manage three.
Local service map:
| Service | URL | Notes |
|---|---|---|
| Backend API | http://localhost:8080 |
the app calls <origin>/v1 |
| Realtime (Phoenix) | ws://localhost:4000/socket/websocket |
fetched automatically via POST /v1/getaway |
| Mailpit (dev email) | http://localhost:18025 |
where login OTP codes land (offset from the usual 8025) |
Step 2: run the app
- Open
ios/Warmbly.xcodeprojin Xcode. - Pick the Warmbly scheme and an iOS 26 simulator.
- Run (Cmd-R).
On a simulator the defaults already point at http://localhost:8080, and Info.plist allows local (non-HTTPS) networking via NSAllowsLocalNetworking, so no ATS changes are needed.
First run
The app opens on an animated welcome screen; Get started begins account creation, I already have an account goes to sign-in. The gear button (top-right on the welcome screen, and on the email step's card) opens Connection settings:
- Server origin — bare origin, no path. Default
http://localhost:8080. The app appends/v1itself. - Turnstile token — pre-filled with the dev bypass token
warmbly-local-turnstile-bypass. The dev backend accepts it so you don't need a real captcha widget. (Production is captcha-gated and needs a real token.)
Then sign in with a seeded account:
| Password | Workspace flavor | |
|---|---|---|
dev@warmbly.com |
password123 |
baseline (has mailboxes, campaigns, warmup data) |
alex@acme.test |
password123 |
free tier |
beth@beta.test |
password123 |
pro |
gus@gamma.test |
password123 |
dedicated worker |
Password login is two-step by design:
- Enter email + password → the backend emails a 6-digit code.
- Open Mailpit at
http://localhost:18025, find the newest message, and enter the code.
After login the app resolves your workspace (auto-selects if you have exactly one, otherwise shows a picker), then lands on the tab bar. You can also register a brand-new account from the app; registration is the same two-step OTP flow, and the confirmation code is in Mailpit too. A brand-new account (password or social) goes through a short onboarding questionnaire (name, how you found us, optional role and team size) before entering the app.
Social sign-in (Apple / Google)
The email step shows native Sign in with Apple and Continue with Google buttons based on what the connected backend advertises at GET /v1/auth/providers:
- Apple — enabled whenever the backend has an expected bundle ID (
APPLE_IOS_BUNDLE_ID, defaultcom.warmbly.app). The app sends the Apple identity token toPOST /v1/auth/apple, which verifies it against Apple's public keys. Trying it end-to-end needs a signed build and a simulator/device signed into an Apple ID. - Google — hidden until the backend sets
GOOGLE_IOS_CLIENT_ID(an iOS OAuth client ID from Google Cloud Console). The app runs the OAuth ceremony in an in-app browser with PKCE, exchanges the code on device (iOS clients have no secret), and sends the ID token toPOST /v1/auth/google.
First-time social sign-ins auto-provision the user, workspace, and trial server-side; there is no OTP step because the provider identity is already verified.
Pointing at another host (staging, self-hosted)
The server is an in-app runtime setting, not a build-time constant, so one binary serves everyone: the hosted SaaS, a self-hosted instance, or a local stack. Users switch hosts from the login screen's Connection settings; nobody rebuilds the app to reach their own infrastructure.
- Shipped (Release) builds default the origin to
https://api.warmbly.com, so ordinary customers just log in and never open the setting. - Debug builds default to
http://localhost:8080for the native dev stack. - Enter any other origin (a staging box, a self-hosted instance) in Connection settings; it persists in
UserDefaults. "Reset to default" returns to the build's default. - App Transport Security: a remote/custom host must be
https://. Plainhttp://is allowed only to localhost and LAN/reserved addresses (dev and on-device testing). See the README caveat.
Running on a physical device
- On the connection screen, set the server origin to your Mac's LAN address, e.g.
http://192.168.1.20:8080(find it withipconfig getifaddr en0). - Make sure
make backendis reachable on the LAN. It binds0.0.0.0:8080by default, so it usually is; check any host firewall. - Realtime is auto-derived from the backend's
WEBSOCKET_URL, which points at:4000. If you run on a device, start the backend withPUBLIC_HOST=<your-lan-ip>(or the equivalent) so the socket URL the backend hands back is reachable from the phone, notlocalhost.
Verifying realtime works
Live updates and presence are a core behavior, so it's worth confirming:
- The More tab shows a connection status row: emerald pulsing "Live" when the socket is connected, amber "Connecting", red "Offline".
- Open the same workspace in the web dashboard (
make web) and in the app. Editing a contact or starting a campaign in one should refresh the other's list within a second, and you should see each other's presence avatars on shared records. - If it stays amber/red: confirm
make realtimeis running and its/healthresponds (curl http://localhost:4000/health), and that the backend'sWEBSOCKET_URLresolves from the device/simulator.
Making changes to the app
Because there is no iOS SDK on CI-less machines, the fast local signal for Swift changes is a syntax parse (it won't typecheck against UIKit/SwiftUI, but it catches structural errors):
cd ios && find Warmbly -name '*.swift' -print0 | xargs -0 swiftc -parse
A real typecheck happens when you build in Xcode. Conventions to keep the code consistent:
- New Codable models declare explicit
CodingKeys(the decoder does not convert snake_case) and copy the Go json tags exactly. Prefer optional fields so a missing key never fails a whole screen. - Reach the app graph with
@Environment(AppEnvironment.self) private var env→env.api(the/v1client),env.session,env.realtime,env.badges. - Every list: load with
.task, reload on the matchingenv.realtime.pulse(for:), and support.refreshable. Gate mutating actions withenv.session.can(.somePermission). - Copy is sentence case, terse, and avoids em dashes.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| "Couldn't reach the server" | make backend not running, or wrong origin. Test curl http://localhost:8080/health. On a device, use the LAN IP, not localhost. |
| Login says the code is invalid/expired | The email session is 10 minutes and allows 3 attempts. Use the newest Mailpit message; start over if it expired. |
| No OTP email in Mailpit | Confirm make infra is up and the backend is sending to Mailpit's SMTP (:11025). Check the backend log for send errors. |
| Stuck on the workspace picker | Your account has 0 or several orgs. Create one or pick one; a fresh login always starts with no org selected and the app switches you in. |
| Lists never update live | Realtime not connected. See Verifying realtime works. |
| A screen is empty for a teammate account | Permission-gated. Some data (e.g. campaign analytics) is owner-only server-side; the app surfaces this rather than erroring. |
| Captcha error on login | The bypass token was cleared. Reset it to warmbly-local-turnstile-bypass in Connection settings. |
What stays on the web
By design, a few flows link out instead of being reimplemented on mobile: mailbox OAuth connect (Gmail/Outlook), campaign sequence editing, and plan changes/billing management. SMTP/IMAP mailbox connect does work natively. See the README for the full list.