Skip to main content

Mobile SDK Considerations

This guide covers security requirements and best practices for using Staqr APIs from mobile applications (iOS, Android, Flutter).

Critical Security Warning

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

ConsiderationRecommendation
KeychainUse for token storage
App Transport SecurityEnable (HTTPS only)
Certificate PinningImplement for production
Jailbreak DetectionConsider for sensitive apps

Android

ConsiderationRecommendation
EncryptedSharedPreferencesUse for token storage
Network Security ConfigPin certificates in production
ProGuardEnable for release builds
Root DetectionConsider for sensitive apps

Flutter

ConsiderationRecommendation
flutter_secure_storageUse for token storage
dioUse with interceptors for auth
Certificate PinningUse 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:

PlatformLanguageStatusDocumentation
iOSSwiftAvailableSwift SDK Guide
AndroidKotlinAvailableKotlin SDK Guide
FlutterDartAvailableDart/Flutter SDK Guide
React NativeTypeScriptUse TypeScript SDKTypeScript 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
Recommended Approach

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