24 KiB
Local iOS Testing with ngrok (notification-wakeup-service)
Last updated: 2026-05-18
Audience: Developers on crowd-funder-for-time-pwa, daily-notification-plugin, and notification-wakeup-service
Goal: Exercise silent push wake (WAKEUP_PING), FCM token registration, and notification refresh against a Mac-hosted backend reachable from a physical iPhone.
Architecture overview
End-to-end flow when testing New Activity / silent wake on a physical iPhone:
┌─────────────────────┐ HTTPS ┌──────────────────────┐
│ Mac (localhost) │ ◄───────────── │ ngrok edge │
│ notification- │ tunnel │ (public HTTPS URL) │
│ wakeup-service │ └──────────┬───────────┘
└──────────┬──────────┘ │
│ │ fetch
│ POST /notifications/refresh │ POST /notifications/register
│ ▼
│ ┌──────────────────────┐
│ │ crowd-funder-for- │
│ │ time-pwa (Capacitor │
│ │ iOS on iPhone) │
│ └──────────┬───────────┘
│ │
│ FCM data message (WAKEUP_PING) │ daily-notification-plugin
▼ ▼ (local schedule replace)
┌─────────────────────┐ ┌──────────────────────┐
│ Firebase Cloud │ ──APNs──────► │ iPhone (physical) │
│ Messaging │ silent push │ app.timesafari │
└─────────────────────┘ └──────────────────────┘
Repos and responsibilities
| Repo | Role |
|---|---|
| notification-wakeup-service | HTTP API: device registration, refresh payload (nextNotifications), health, debug wakeup send |
| crowd-funder-for-time-pwa | Capacitor app: FCM token, POST /notifications/register & /refresh, handles WAKEUP_PING push |
| daily-notification-plugin | Native iOS/Android: clear + reschedule local notifications from refresh timestamps |
Silent wake sequence (production path)
- Backend (or
/debug/send-wakeup) sends an FCM data message withdata.type = "WAKEUP_PING". - APNs delivers to the device (best-effort; see iOS caveats below).
- Capacitor
pushNotificationReceivedfires →handleCapacitorPushNotificationReceived(). - App calls
POST {backend}/notifications/refreshwithtestMode(from debug config). - Backend returns
nextNotifications: [{ timestamp }, ...]. - App calls
applyNotificationRefreshPayload()→ plugin clears and schedules new local alarms.
Console and debug panel lines are prefixed with [Notifications] (see NotificationDebugEvents.ts).
Prerequisites
- Mac with Xcode, Node.js 18+, and the notification-wakeup-service repo cloned and runnable
- Physical iPhone (USB or wireless debugging) — simulator is not sufficient for reliable silent push / APNs behavior
- ngrok account (free tier is enough for dev)
- Firebase project with APNs configured for the iOS app bundle ID
- Non-production app build (Notification Debug Panel is dev-only)
1. Install and configure ngrok (macOS)
Install
# Homebrew
brew install ngrok/ngrok/ngrok
Or download from https://ngrok.com/download.
Account and auth token
- Sign up at https://dashboard.ngrok.com/signup.
- Copy your authtoken from Your Authtoken in the dashboard.
- Configure the CLI:
ngrok config add-authtoken YOUR_AUTHTOKEN_HERE
Start a tunnel to the wakeup service
Assume the service listens on port 3000 (confirm in notification-wakeup-service README or .env).
If the service already defaults to port 3000 internally, you may not need to export PORT manually.
# Terminal A — backend
cd /path/to/notification-wakeup-service
npm install
# one-time setup if needed
cp .env.example .env
# configure Firebase/service account/etc as required
export PORT=3000
npm run dev
# Terminal B — ngrok
ngrok http 3000
The backend only needs to be started once. The dedicated backend section below exists for verification and troubleshooting details, not as a second startup step.
ngrok prints a forwarding URL, for example:
Forwarding https://abc123.ngrok-free.app -> http://localhost:3000
Use the HTTPS URL (not http://127.0.0.1:3000). The iPhone cannot reach your Mac’s localhost without the tunnel.
Note: Free ngrok URLs change every time you restart ngrok unless you use a reserved domain (paid). Update the app debug override whenever the URL changes.
2. Start the backend locally
Example (adjust to match notification-wakeup-service). On first setup, copy .env.example to .env and set Firebase service account, PORT, and other variables per that repo's docs.
If the backend is not already running from section 1:
# If not already running from the previous step:
cd /path/to/notification-wakeup-service
npm run dev
Verify locally before ngrok:
curl -sS http://localhost:3000/health
Expected: HTTP 200 and a JSON body indicating the service is up (exact shape depends on that repo).
3. Obtain and use the ngrok HTTPS URL
- Run
ngrok http <PORT>. - Copy the
https://….ngrok-free.apphost from the Forwarding line. - Do not add a trailing slash when saving in the app (the debug config trims it).
- Optional: open
http://127.0.0.1:4040(ngrok web UI) to inspect requests and responses while testing.
Test through the tunnel from your Mac:
export NGROK_URL="https://abc123.ngrok-free.app"
curl -sS "$NGROK_URL/health"
4. Generate and open the iOS workspace
From crowd-funder-for-time-pwa, generate the Capacitor iOS project and open it in Xcode. Section 5 (Firebase + APNs) needs this workspace—for example to add GoogleService-Info.plist and enable Push Notifications in the app target. The app does not need Firebase or push fully configured yet; the goal here is a buildable Xcode project on your Mac.
npm install
npm run build:ios:dev # or build:ios:test — non-production for debug panel
Open the generated Xcode workspace (for example ios/App/App.xcworkspace), select your physical iPhone, enable signing, and Run when you are ready to verify the app launches.
Ensure VITE_FIREBASE_* variables are set for the Capacitor build you use (see .env / build docs). Native push registration runs at startup via initializeNativePushAndFirebaseMessaging() in main.capacitor.ts once Firebase is configured in the next section.
5. Firebase + APNs setup (first-time setup)
Complete this section once before your first physical-device push test. If Firebase and APNs are already configured for this app, skip to section 6.
Create or access a Firebase account
-
Sign in with a Google account at https://console.firebase.google.com/.
-
If this is your first time using Firebase:
- Accept the Firebase terms.
- Create a new Firebase account/workspace when prompted.
-
No paid Firebase plan is required for local iOS notification testing. The free Spark plan is sufficient for:
- Firebase Cloud Messaging (FCM)
- APNs silent push testing
- local ngrok-based development
Create a Firebase project
- In the Firebase Console, click Add project (or Create a project).
- Enter a project name (for example,
timesafari-dev) and continue through the wizard. - Google Analytics is optional for this workflow; you can disable it for a simpler dev project.
- When the project is created, open it. Cloud Messaging is available on all projects — you do not need a separate enable step for FCM.
Register the iOS app in Firebase
- In the project overview, click the iOS icon (Add app → iOS).
- Enter the Apple bundle ID. It must exactly match the Capacitor / Xcode app ID:
app.timesafari(seeappIdincapacitor.config.tsand the Xcode target Bundle Identifier).
- App nickname and App Store ID are optional for local testing; continue.
- Download
GoogleService-Info.plistwhen prompted and keep it handy for the next step.
Add GoogleService-Info.plist to Xcode
- Open the iOS workspace you generated in section 4 (for example
ios/App/App.xcworkspace). - In the Project Navigator, drag
GoogleService-Info.plistinto the App folder (the same one that contains AppDelegate.swift and Info.plist). - In the dialog that appears:
- Check Copy items if needed (so the file is copied into the project tree).
- Under Add to targets, ensure the main app target (not only the share extension) is checked.
- Confirm the file appears under the app target in Xcode and is listed in Build Phases → Copy Bundle Resources if your project uses that phase for plists.
Create an APNs Authentication Key
Apple uses APNs to deliver pushes to devices; Firebase needs an APNs key to talk to Apple on your behalf.
- Sign in to Apple Developer → Certificates, Identifiers & Profiles.
- Open Keys → + (create a new key).
- Name the key (for example,
Timesafari APNs Dev). - Enable Apple Push Notifications service (APNs) and continue.
- Register the key, then Download the
.p8file. You can download it only once — store it securely. - Note:
- Key ID (shown on the key detail page)
- Team ID (top right of the developer portal, or Membership details)
Upload APNs key to Firebase
- Firebase Console → your project → Project settings (gear icon).
- Open the Cloud Messaging tab.
- Under Apple app configuration, select your iOS app (
app.timesafari) if prompted. - Under APNs Authentication Key, click Upload.
- Select the
.p8file and enter:- Key ID
- Team ID
- Save. Firebase can now send FCM messages through APNs to your iOS app.
Enable iOS capabilities in Xcode
- Select the App target → Signing & Capabilities.
- Click + Capability and add Push Notifications.
- Click + Capability again and add Background Modes.
- Under Background Modes, enable Remote notifications.
These match what silent / data wake flows expect for background delivery.
Configure Firebase Admin for the backend
notification-wakeup-service uses the Firebase Admin SDK to send FCM (and thus APNs) messages from your Mac.
- Firebase Console → Project settings → Service accounts.
- Click Generate new private key and confirm download of the JSON file.
- Store the JSON outside the repo (do not commit it).
- Point the backend at it, for example:
export GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/to/service-account.json"
The backend uses this credential to authenticate with Firebase when calling endpoints such as /debug/send-wakeup. Set the same variable (or the equivalent env var documented in notification-wakeup-service) in the shell where you run npm run dev, or add it to that repo’s .env per its README.
Verify Firebase configuration
Before ngrok end-to-end testing, confirm:
- App builds and launches on a physical iPhone without Firebase/plist errors in Xcode.
- iOS shows the push permission prompt (or Settings → app → Notifications is enabled).
- Notification Debug Panel shows an FCM token (after permission).
- Register Token Now succeeds and ngrok (or local backend) shows
POST /notifications/register. - Backend health and Firebase Admin env are set so
/debug/send-wakeupcan run when you reach that step in the workflow below.
6. Configure the Notification Debug Panel backend override
The app normally calls DEFAULT_NOTIFY_API_SERVER (from VITE_DEFAULT_NOTIFY_API_SERVER, falling back to AppString.PROD_NOTIFY_API_SERVER). That is independent of APP_SERVER. For local wakeup testing, override the notification API base URL in the Debug Panel without rebuilding.
For a full panel reference (configuration, URL resolution order, authentication, and troubleshooting), see notification-debug-panel.md.
Open the panel
- Use a non-production bundle (e.g. dev/test build).
- Account → enable Show All General Advanced Functions.
- Open Notification Debug Panel (route
/dev/notifications).
Backend Testing section
| Control | Purpose |
|---|---|
| Notification Backend URL | Paste ngrok HTTPS URL → Save Backend URL (changes target server only) |
| Test Mode | Sends testMode: true/false in register/refresh JSON bodies (default on when unset in storage) |
| Skip JWT Authentication (Local Development Only) | When on, omits Authorization headers for local servers that accept unauthenticated requests (default off) |
| Register Token Now | POST /notifications/register with current FCM token |
| Refresh Notifications | POST /notifications/refresh (same as post-wakeup flow) |
| Simulate WAKEUP_PING (Local) | Calls refresh API directly (no FCM) — quick backend test |
| Send Real WAKEUP_PING | POST /debug/send-wakeup; server sends real FCM WAKEUP_PING (Android doc has full flow) |
| Event Log | Shared [Notifications] panel log (100 entries) |
Persistence: localStorage keys notificationDebug.backendBaseUrl, notificationDebug.testMode, and notificationDebug.bypassAuth (NotificationDebugConfig.ts).
Authentication vs backend URL
These settings are independent:
- Backend URL — which server receives notification API calls.
- Test Mode —
testModefield in JSON request bodies only. - Skip JWT Authentication — whether JWT
Authorizationheaders are sent.
For a hosted shared test server: set the backend URL, keep Test Mode on if required, leave Skip JWT Authentication off, and ensure an active DID exists.
For local ngrok: set the backend URL; enable Skip JWT Authentication only if your local backend accepts unauthenticated requests.
Programmatic override (optional)
From Safari Web Inspector or a dev console attached to the WebView:
import {
setBackendBaseUrl,
setTestMode,
setBypassAuth,
getNotificationApiBaseUrl,
} from "@/services/notifications";
setBackendBaseUrl("https://abc123.ngrok-free.app");
setTestMode(true);
setBypassAuth(true); // local dev only — omit for hosted servers that require JWT
getNotificationApiBaseUrl(); // → ngrok URL
7. Firebase and Xcode checklist (iOS)
This section is a quick verification checklist for the detailed Firebase/APNs setup steps above.
| Item | Action |
|---|---|
| Bundle ID | Match Capacitor appId (app.timesafari in capacitor.config.ts) to Firebase iOS app and Xcode target |
| APNs auth key | Firebase Console → Project Settings → Cloud Messaging → upload APNs Authentication Key (.p8) or certificates |
| Push Notifications | Xcode target → Signing & Capabilities → + Capability → Push Notifications |
| Background Modes | Enable Remote notifications (and any others required by your plugin docs) |
| GoogleService-Info.plist | Present in the iOS target if using Firebase iOS SDK paths in your build |
| FCM token | Confirm Register Token Now succeeds in the debug panel and ngrok shows POST /notifications/register |
Silent/data pushes used for wake typically use a content-available style payload; confirm notification-wakeup-service and Firebase message format match what handleCapacitorPushNotificationReceived expects (data.type === "WAKEUP_PING").
8. iOS-specific testing notes
Physical device required
- APNs silent delivery and background wake behavior are not representative on the iOS Simulator.
- Always validate on a plugged-in or trusted wireless device with a development provisioning profile.
Silent push is best-effort
- iOS may delay or coalesce background pushes, especially on battery saver or under load.
- A successful
/debug/send-wakeupfrom the server does not guarantee immediate app wake.
Force-quit limitations
- If the user swipes the app away from the app switcher, iOS often will not deliver background notifications until the user launches the app again.
- Test with the app backgrounded (home button / gesture), not force-quit, when validating wake.
Low Power Mode and Focus
- Low Power Mode can reduce background execution.
- Focus / Do Not Disturb may affect notification presentation (separate from silent data wake, but confusing during tests).
Two “Simulate WAKEUP_PING” buttons
| Button | Behavior |
|---|---|
| Backend Testing → Simulate WAKEUP_PING | Skips FCM; calls refresh API only (ngrok path test) |
| Wakeup Ping Simulator (lower on panel) | Runs production handler with synthetic WAKEUP_PING payload |
Use the backend button to verify ngrok + refresh; use the simulator to verify handler + refresh chaining.
9. Recommended debug workflow
- Start notification-wakeup-service on the Mac.
- Start ngrok and copy the HTTPS URL.
- Set URL + Test Mode in the Notification Debug Panel; confirm Backend Status.
- Tap Register Token Now → confirm ngrok request and
[Notifications] Token registration success. - Tap Refresh Notifications → confirm
Refresh completed in Nms (scheduled X)in Event Log and ngrokPOST /notifications/refresh. - From the backend, call
/debug/send-wakeup(see curl below) with the registereddeviceId/ FCM token as required by that service. - Watch Xcode console for
[Notifications] pushNotificationReceived type=WAKEUP_PINGand refresh timing lines. - Open ngrok inspect UI (
http://127.0.0.1:4040) to correlate requests. - Use Pending Notification Inspector on the panel to see locally scheduled fires after refresh.
10. Sample curl commands
Set your tunnel base URL:
export BASE="https://abc123.ngrok-free.app"
Health
curl -sS -w "\nHTTP %{http_code}\n" "$BASE/health"
Register device (mirror app payload)
curl -sS -X POST "$BASE/notifications/register" \
-H "Content-Type: application/json" \
-d '{
"deviceId": "00000000-0000-4000-8000-000000000001",
"fcmToken": "YOUR_FCM_TOKEN_FROM_DEBUG_PANEL",
"platform": "ios",
"testMode": true
}'
Refresh (mirror app payload)
curl -sS -X POST "$BASE/notifications/refresh" \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"testMode": true
}'
Example success body shape (actual fields may vary by service version):
{
"shouldNotify": true,
"nextNotifications": [
{ "timestamp": 1710000000000 },
{ "timestamp": 1710003600000 }
]
}
The app schedules those timestamps via daily-notification-plugin (applyNotificationRefreshPayload in NativeNotificationService.ts).
Send wakeup push (debug)
Exact path and body depend on notification-wakeup-service; typical pattern:
curl -sS -X POST "$BASE/debug/send-wakeup" \
-H "Content-Type: application/json" \
-d '{
"deviceId": "00000000-0000-4000-8000-000000000001",
"testMode": true
}'
Confirm parameters (token vs deviceId, auth headers) in that repo’s README or OpenAPI spec.
11. Troubleshooting
Refresh endpoint unreachable
| Symptom | Checks |
|---|---|
| Network error in Event Log | ngrok running? URL saved without typo/trailing slash? |
| HTTP 404 | Tunnel port matches backend PORT; path is /notifications/refresh |
| CORS (web only) | Native Capacitor fetch usually avoids browser CORS; if testing in Safari PWA, configure CORS on the service |
| ngrok browser warning | Free tier may show an interstitial for browser clients; native fetch from the app is usually unaffected |
Token registration failures
- Push permission granted on the device?
- Firebase
VITE_FIREBASE_*env vars baked into the build? [Notifications] Token registration failurein Xcode — read HTTP status in ngrok inspect- Duplicate token skip: panel may show “skipped (duplicate)”; use Register Token Now to force re-register
Silent push not waking the app
- App backgrounded, not force-quit
- Physical device, correct provisioning profile
- APNs key uploaded to Firebase; bundle ID matches
- FCM message includes
data.type = "WAKEUP_PING"(seeNativeNotificationService.ts) - Server actually sent to the same FCM token shown in the debug panel
- Wait 30–120s — delivery is not instant
- Try Simulate WAKEUP_PING (refresh API) to isolate app/plugin from FCM/APNs
Notifications duplicating
- Multiple refresh calls (flood test, repeated wakeups) each replace schedule via clear + schedule — check Event Log for repeated refreshes
- Separate issue: Daily Reminder vs New Activity both scheduling — see
doc/notification-new-activity-lay-of-the-land.md
Stale ngrok URL
- After restarting ngrok, update Notification Backend URL in the panel and tap Save
- Or clear override (empty field + Save) only if you intend to hit
DEFAULT_NOTIFY_API_SERVERagain
Plugin / JWT errors after refresh
- Refresh calls
configureNativeFetcherIfReady()before scheduling — ensure an active DID and endorser API settings exist in the app DB - See
doc/notification-from-api-call.mdandnativeFetcherConfig.ts
12. Key source files (crowd-funder-for-time-pwa)
| File | Purpose |
|---|---|
src/services/notifications/NotificationDebugConfig.ts |
Backend URL, testMode, and bypassAuth overrides |
src/services/notifications/NotificationDebugEvents.ts |
Panel event log + logNotification() |
src/services/notifications/notificationLog.ts |
Structured log helpers |
src/services/notifications/NotificationService.ts |
POST /notifications/register |
src/services/notifications/NativeNotificationService.ts |
Refresh, WAKEUP_PING, schedule replace |
src/services/notifications/firebaseMessagingClient.ts |
Capacitor push listeners |
src/components/dev/NotificationDebugPanel.vue |
Dev UI |
src/main.capacitor.ts |
Native push init at startup |
13. Related docs
- notification-debug-panel.md — panel controls, authentication, troubleshooting
- Notification Debug Panel (README)
- notification-system-overview.md
- notification-from-api-call.md
- notification-new-activity-lay-of-the-land.md
- BUILDING.md — iOS build commands
For plugin-native behavior (exact alarm, iOS pending inspector), see daily-notification-plugin documentation. For FCM payload format and /debug/send-wakeup contract, see notification-wakeup-service.