Mobile SDK Considerations
This guide covers security requirements and best practices for using Staqr APIs from mobile applications (iOS, Android, Flutter).
NEVER embed API keys directly in mobile applications.
Mobile apps can be decompiled and API keys extracted. This exposes your entire account to attackers. Always use a backend proxy pattern.
The Problem with Mobile API Keys
Why Direct API Access is Dangerous
┌─────────────┐ Direct API Call ┌─────────────┐
│ Mobile App │ ────────────────────────> │ Staqr API │
│ (API Key │ KEY EXPOSED! │ │
│ embedded) │ │ │
└─────────────┘ └─────────────┘
↓
Attacker decompiles APK/IPA
Extracts API key
Makes unlimited API calls
Incurs charges to your account
Real risks:
- API keys visible in app binary (even "obfuscated")
- Network traffic can be intercepted (even with SSL)
- Keys cannot be rotated without app store update
- Single compromised device exposes all customers
The Backend Proxy Pattern
The recommended architecture for mobile apps:
┌─────────────┐ App Auth ┌─────────────┐ API Key ┌─────────────┐
│ Mobile App │ ────────────> │ Your Backend│ ───────────> │ Staqr API │
│ (NO keys) │ │ (API Key │ │ │
│ │ <──────────── │ stored │ <─────────── │ │
└─────────────┘ Response │ securely) │ Response └─────────────┘
└─────────────┘
Benefits:
- API keys never leave your servers
- Keys can be rotated without app updates
- You control rate limiting and access
- Additional security layers possible (IP filtering, etc.)
Implementation Examples
Backend Proxy (Node.js/Express)
// backend/routes/staqr-proxy.ts
import express from 'express';
import { Configuration, FlowsApi } from '@staqr/staqr-api';
const router = express.Router();
// Your API key - stored securely on backend
const config = new Configuration({
basePath: process.env.STAQR_BASE_URL,
accessToken: process.env.STAQR_API_KEY
});
// Authenticate mobile users with YOUR auth system
const authenticateUser = (req, res, next) => {
// Verify Firebase token, JWT, etc.
const userToken = req.headers.authorization?.replace('Bearer ', '');
if (!verifyUserToken(userToken)) {
return res.status(401).json({ error: 'Unauthorized' });
}
next();
};
// Proxy endpoint - list flows
router.get('/flows', authenticateUser, async (req, res) => {
try {
const api = new FlowsApi(config);
const flows = await api.listFlows();
res.json(flows);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch flows' });
}
});
// Proxy endpoint - execute flow
router.post('/flows/:id/execute', authenticateUser, async (req, res) => {
try {
const api = new FlowsApi(config);
const result = await api.executeFlow({
flowId: req.params.id,
payload: req.body
});
res.json(result);
} catch (error) {
res.status(500).json({ error: 'Failed to execute flow' });
}
});
export default router;
Mobile App (React Native)
// services/staqr.ts
const API_BASE = 'https://your-backend.com/api/staqr';
async function getAuthToken(): Promise<string> {
// Get token from your auth system (Firebase, Auth0, etc.)
return await auth().currentUser?.getIdToken();
}
export async function listFlows() {
const token = await getAuthToken();
const response = await fetch(`${API_BASE}/flows`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error('Failed to list flows');
}
return response.json();
}
export async function executeFlow(flowId: string, payload: object) {
const token = await getAuthToken();
const response = await fetch(`${API_BASE}/flows/${flowId}/execute`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error('Failed to execute flow');
}
return response.json();
}
Mobile App (Flutter)
// services/staqr_service.dart
import 'package:dio/dio.dart';
import 'package:firebase_auth/firebase_auth.dart';
class StaqrService {
final Dio _dio;
static const String _baseUrl = 'https://your-backend.com/api/staqr';
StaqrService() : _dio = Dio(BaseOptions(baseUrl: _baseUrl));
Future<String> _getAuthToken() async {
final user = FirebaseAuth.instance.currentUser;
return await user?.getIdToken() ?? '';
}
Future<List<dynamic>> listFlows() async {
final token = await _getAuthToken();
final response = await _dio.get(
'/flows',
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
return response.data;
}
Future<Map<String, dynamic>> executeFlow(String flowId, Map<String, dynamic> payload) async {
final token = await _getAuthToken();
final response = await _dio.post(
'/flows/$flowId/execute',
data: payload,
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
return response.data;
}
}
Token Management in Mobile Apps
Short-Lived Tokens
For additional security, use short-lived tokens from your backend:
// Backend: Issue short-lived tokens
router.post('/auth/mobile-token', async (req, res) => {
const { userToken } = req.body;
// Verify user's auth token
const user = await verifyUserToken(userToken);
if (!user) {
return res.status(401).json({ error: 'Invalid user token' });
}
// Generate short-lived token for Staqr operations
const mobileToken = jwt.sign(
{ userId: user.id, permissions: ['flows:read', 'flows:execute'] },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
res.json({ token: mobileToken, expiresIn: 900 });
});
Secure Token Storage
iOS:
import Security
func saveToken(_ token: String) {
let data = token.data(using: .utf8)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "staqr_token",
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemAdd(query as CFDictionary, nil)
}
Android:
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
fun saveToken(context: Context, token: String) {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val sharedPrefs = EncryptedSharedPreferences.create(
context,
"staqr_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
sharedPrefs.edit().putString("token", token).apply()
}
Platform-Specific Considerations
iOS
| Consideration | Recommendation |
|---|---|
| Keychain | Use for token storage |
| App Transport Security | Enable (HTTPS only) |
| Certificate Pinning | Implement for production |
| Jailbreak Detection | Consider for sensitive apps |
Android
| Consideration | Recommendation |
|---|---|
| EncryptedSharedPreferences | Use for token storage |
| Network Security Config | Pin certificates in production |
| ProGuard | Enable for release builds |
| Root Detection | Consider for sensitive apps |
Flutter
| Consideration | Recommendation |
|---|---|
| flutter_secure_storage | Use for token storage |
| dio | Use with interceptors for auth |
| Certificate Pinning | Use http_certificate_pinning package |
Offline Considerations
Mobile apps often need to work offline. Consider these patterns:
Caching Strategy
// Mobile app caching example
import AsyncStorage from '@react-native-async-storage/async-storage';
async function getFlowsWithCache() {
const cacheKey = 'flows_cache';
const cacheExpiry = 5 * 60 * 1000; // 5 minutes
// Try to get cached data
const cached = await AsyncStorage.getItem(cacheKey);
if (cached) {
const { data, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < cacheExpiry) {
return data;
}
}
// Fetch fresh data
try {
const flows = await listFlows();
await AsyncStorage.setItem(cacheKey, JSON.stringify({
data: flows,
timestamp: Date.now()
}));
return flows;
} catch (error) {
// Return stale cache if network fails
if (cached) {
return JSON.parse(cached).data;
}
throw error;
}
}
Offline Queue
// Queue actions when offline
class OfflineQueue {
private queue: Array<{action: string, payload: object}> = [];
async enqueue(action: string, payload: object) {
this.queue.push({ action, payload });
await AsyncStorage.setItem('offline_queue', JSON.stringify(this.queue));
}
async processQueue() {
const stored = await AsyncStorage.getItem('offline_queue');
if (!stored) return;
this.queue = JSON.parse(stored);
for (const item of this.queue) {
try {
await this.processAction(item);
this.queue = this.queue.filter(q => q !== item);
} catch (error) {
console.log('Action failed, will retry later');
break;
}
}
await AsyncStorage.setItem('offline_queue', JSON.stringify(this.queue));
}
}
Native Mobile SDKs
Native SDKs are now available for mobile platforms:
| Platform | Language | Status | Documentation |
|---|---|---|---|
| iOS | Swift | Available | Swift SDK Guide |
| Android | Kotlin | Available | Kotlin SDK Guide |
| Flutter | Dart | Available | Dart/Flutter SDK Guide |
| React Native | TypeScript | Use TypeScript SDK | TypeScript SDK Guide |
These SDKs include:
- Built-in secure token management patterns
- Platform-specific authentication examples
- State management integration (SwiftUI, Jetpack Compose, Provider/Riverpod/Bloc)
- Error handling and offline support patterns
While native SDKs are available, we still recommend the backend proxy pattern for production apps to keep API keys secure. Use the native SDKs with OAuth2/OIDC user authentication or short-lived tokens from your backend.
Checklist: Mobile Security
Before releasing your mobile app:
- No API keys in app code - Use backend proxy
- Secure token storage - Keychain (iOS) or EncryptedSharedPreferences (Android)
- HTTPS only - No HTTP connections allowed
- Certificate pinning - Pin your backend's certificate
- Token expiry - Use short-lived tokens
- User authentication - Verify user identity before API calls
- Rate limiting - Implement on your backend
- Error handling - Don't expose sensitive info in errors
Next Steps
- Swift SDK Guide - Native iOS/macOS development
- Kotlin SDK Guide - Native Android development
- Dart/Flutter SDK Guide - Cross-platform Flutter development
- Authentication Guide - Token management patterns
- TypeScript SDK - For backend proxy implementation