Build a native mobile app¶
A native iOS or Android app on top of SweatStack has three parts that differ from a server-side or web integration:
- How the user authenticates from the device.
- Where the access tokens live.
- How the app receives updates from SweatStack's webhooks.
This guide covers all three.
What you'll build¶
An architecture for a native mobile app that:
- Authenticates users with SweatStack through OAuth2 with PKCE, in the platform's system browser session
- Stores tokens securely on the device and refreshes them without involving the user
- Receives push notifications when activities or tests change, through a small server-side gateway
1. Authentication on the device¶
SweatStack uses standard OAuth2 with PKCE for native apps. The device needs no SweatStack-specific SDK. Use the OS-provided web auth components or any OAuth2/OIDC library that supports PKCE.
Recommended components¶
| Platform | Auth session | Reason |
|---|---|---|
| iOS | ASWebAuthenticationSession |
System-managed, shares cookies with Safari, handles redirect dismissal automatically. |
| Android | Custom Tabs (via AppAuth-Android) | Same pattern: system browser, cookie-shared, redirect handling. |
Both flows are identical from SweatStack's side. The device opens an authorization URL, the user authenticates with SweatStack (or through Sign in with SweatStack, with their wearable account), and the redirect URI returns control to your app with an authorization code.
Redirect URI options¶
Two options work for native apps. Pick one and register it as a redirect URI in your app configuration.
- Custom URL scheme. Register a scheme like
com.example.myapp://oauth/callback. Simpler to set up, and it works on any iOS or Android version. SweatStack matches custom schemes exactly, so register the precise URI your app redirects to (see Redirect URIs). - Universal links (iOS) and App Links (Android). Use a real
https://URL that the OS routes to your app through your domain'sapple-app-site-associationorassetlinks.json. More setup, but harder to phish, and the modern recommendation for new apps.
Custom URL schemes are fine for a first version. If you have a domain ready and want the better security posture, use universal links from the start.
HTTPS redirect URIs work directly. No relay needed.
SweatStack accepts https:// redirect URIs at registration. Register your Universal Link or App Link URL (for example https://your-app.example.com/oauth/callback) directly. You do not need an intermediate Cloudflare Worker, web page, or other relay that bounces a custom-scheme callback into the app. The OS does that routing when the link is correctly associated with your app bundle.
Flow¶
+------------------+ +-------------------+
| | 1. open ASWebAuthenticationSession | |
| Native app | ------------------------------------> | System browser |
| | (authorize URL with PKCE) | |
+------------------+ +---------+---------+
^ |
| | 2. user logs in
| | on SweatStack
| 4. redirect URI returns v
| (code + state) +-------------------+
+-------------------------------------------------- | SweatStack |
+-------------------+
+------------------+ +-------------------+
| | 5. POST /oauth/token (code + | |
| Native app | code_verifier) | SweatStack |
| | ------------------------------------> | |
| | <------------------------------------ | |
| | access_token + refresh_token | |
+------------------+ +-------------------+
Important: no client secret on the device¶
PKCE replaces the client secret. Never ship a client secret in a mobile binary. Treat it as a server-only credential. SweatStack's authorization endpoint accepts PKCE without a secret for native flows.
For the OAuth2 and PKCE wire format, see OAuth2.
2. Token storage on the device¶
| Platform | Storage |
|---|---|
| iOS | The Keychain, through SecItemAdd or a wrapper like KeychainAccess. |
| Android | EncryptedSharedPreferences, or a Keystore-backed token store. |
Both keep tokens encrypted at rest and tied to the device. Don't write tokens to plain UserDefaults, files, or SharedPreferences.
Refresh strategy¶
An access token is valid for 15 minutes. A refresh token has no expiry; it stops working when the user revokes the app's permissions. The standard pattern:
- Before an API call, check the access token's
expclaim. It's a JWT, so decode it locally. No network call needed. - If the token has expired or expires within about 30 seconds, refresh it first.
- If a request returns
401, refresh and retry once.
A user who opens the app once a month after a quiet stretch refreshes successfully and never sees a sign-in screen. The full token lifecycle (JWT verification, JWKS, OpenID Connect discovery) is on the OAuth2 page.
3. Updates: from webhooks to push notifications¶
A phone can't expose a public HTTP endpoint, so it can't receive webhooks directly. The standard pattern is a server-side gateway between SweatStack's webhooks and the device's push channel (APNs on iOS, FCM on Android).
Architecture¶
+-------------+ webhook +----------------+ enrich +-------------+
| | POST | | via API | |
| SweatStack | ---------> | Gateway | --------> | SweatStack |
| | | (Worker / | (token | |
| | | Lambda / | lookup) | |
| | | server) | | |
+-------------+ +--------+-------+ +-------------+
|
| push
v
+---------------+
| APNs / FCM |
+-------+-------+
|
v
+---------------+
| Native app |
+---------------+
Gateway implementation¶
A Cloudflare Worker, an AWS Lambda, or any small HTTP service works. The gateway:
- Receives the webhook from SweatStack. It verifies the HMAC signature (see Webhooks) and acknowledges with a
2xxwithin 2 seconds. - Looks up the user's access token. The webhook payload carries
user_id,event_type, andresource_id, but no resource data. To enrich the event with details for the push notification ("New ride: 42 km, 1h 30m"), the gateway calls SweatStack with that user's access token. So the gateway needs a way to fetch tokens byuser_id. A key-value store (Cloudflare KV, DynamoDB, your existing user database) keyed onuser_idis the standard shape. - Looks up the device's push token. The same lookup, also keyed on
user_id. - Sends the push. Through APNs or FCM, with the notification payload it built.
What goes in the payload¶
The webhook payload:
{
"user_id": "string",
"event_type": "activity_created",
"resource_id": "string",
"timestamp": "2026-05-08T12:00:00Z"
}
A useful push notification usually wants richer content: the activity's sport, distance, duration. That is what the enrichment step is for.
Delivery guarantees¶
SweatStack retries a failed webhook delivery eight times over about twelve hours (see Response time requirement). APNs and FCM also retry. Make the gateway idempotent on (resource_id, event_type), so duplicate deliveries don't produce duplicate pushes.
When a phone is offline, APNs and FCM queue notifications (APNs collapses them; FCM stores them up to a configurable TTL). The gateway doesn't need to track this. The push services handle it. For a phone that has been offline for a long time, the app should reconcile its state from SweatStack the next time it comes to the foreground, instead of relying on every push having arrived.
Background sync as a fallback¶
For seasonal apps that users open intermittently, push-driven sync is the right primary mechanism. You can add background fetch (BGTaskScheduler on iOS, WorkManager on Android) for a smoother experience, but the OS controls the scheduling, so it guarantees no freshness. Treat it as best effort while the app is in the background, not as the primary sync.
Next steps¶
- OAuth2: the full OAuth2 and PKCE wire format, token lifecycle, scopes.
- Custom calculations on activities: how to add derived values without running a database, and the rare cases where you need one.
- Webhooks: event types, payload format, signature verification, retries.
- Sign in with SweatStack: the one-button login and onboarding. It works inside
ASWebAuthenticationSessionexactly like a regular OAuth2 login.