Mobile Integration (React Native & Flutter)
Mobile Integration (React Native & Flutter)
This guide covers how to integrate WeftKit into React Native and Flutter mobile applications safely and efficiently.
Architecture: Do Not Connect Directly from Mobile
Mobile applications should never connect directly to WeftKit Standalone. Instead, route all database access through a backend API layer:
Mobile App ──(HTTPS)── Your API Backend ──(TCP)── WeftKit Standalone
(REST/GraphQL) (Node.js / Python / Java / Go)
Direct connections from mobile are problematic because:
| Problem | Consequence | |---|---| | Credentials in the app binary | Extractable via reverse engineering; anyone can connect to your database | | Mobile IPs are untrusted | Database exposed to the public internet | | No connection pooling | Each mobile device holds a persistent TCP connection | | No business logic enforcement | Authorization rules must be re-implemented on every client | | App store reviews take days | A leaked credential cannot be rotated without a new app release |
The recommended backend approach gives you:
- Security: WeftKit credentials never leave your private network
- Connection pooling: Backend reuses a small pool; WeftKit serves hundreds of backends, not millions of phones
- Offline support: The API layer can cache responses and queue writes
- Reduced latency: Your backend is co-located with WeftKit on the same network or VPC
1. React Native — Calling a WeftKit-backed REST API
Your backend exposes REST endpoints; the mobile app calls them using fetch or axios.
Using fetch
typescript// src/api/client.ts const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'https://api.example.com'; async function apiFetch<T>(path: string, options?: RequestInit): Promise<T> { const token = await getStoredToken(); // from secure storage const response = await fetch(`${API_BASE_URL}${path}`, { ...options, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, ...options?.headers, }, }); if (!response.ok) { const error = await response.json().catch(() => ({ message: response.statusText })); throw new ApiError(response.status, error.message); } return response.json() as Promise<T>; }
Using axios
bashnpm install axios
typescript// src/api/axios-client.ts import axios, { AxiosInstance } from 'axios'; import { getStoredToken } from './secure-storage'; export function createApiClient(): AxiosInstance { const client = axios.create({ baseURL: process.env.EXPO_PUBLIC_API_URL, timeout: 10_000, }); // Attach auth token to every request client.interceptors.request.use(async (config) => { const token = await getStoredToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Handle 401 — refresh or redirect to login client.interceptors.response.use( (response) => response, async (error) => { if (error.response?.status === 401) { await clearStoredToken(); // Navigate to login screen } return Promise.reject(error); } ); return client; } export const apiClient = createApiClient();
TypeScript Types for API Responses
typescript// src/types/api.ts export interface User { id: number; name: string; email: string; created_at: string; } export interface Product { id: number; name: string; description: string; price: number; in_stock: boolean; image_url: string | null; } export interface PaginatedResponse<T> { data: T[]; total: number; page: number; per_page: number; has_more: boolean; } export interface ApiError { status: number; message: string; code?: string; }
Login Flow
typescript// src/api/auth.ts import { apiClient } from './axios-client'; import { storeToken, clearStoredToken } from './secure-storage'; import type { User } from '../types/api'; interface LoginResponse { token: string; expires_in: number; user: User; } export async function login(email: string, password: string): Promise<User> { const { data } = await apiClient.post<LoginResponse>('/auth/login', { email, password, }); await storeToken(data.token); return data.user; } export async function logout(): Promise<void> { await apiClient.post('/auth/logout').catch(() => {}); // best-effort await clearStoredToken(); }
Loading a Product List
typescript// src/api/products.ts import { apiClient } from './axios-client'; import type { Product, PaginatedResponse } from '../types/api'; export async function listProducts(params: { page?: number; per_page?: number; category?: string; }): Promise<PaginatedResponse<Product>> { const { data } = await apiClient.get<PaginatedResponse<Product>>('/products', { params, }); return data; } export async function getProduct(id: number): Promise<Product> { const { data } = await apiClient.get<Product>(`/products/${id}`); return data; }
typescript// Usage in a React Native component import React, { useEffect, useState } from 'react'; import { FlatList, Text, View, ActivityIndicator } from 'react-native'; import { listProducts } from '../api/products'; import type { Product } from '../types/api'; export function ProductListScreen() { const [products, setProducts] = useState<Product[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { listProducts({ page: 1, per_page: 20 }) .then(({ data }) => setProducts(data)) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); }, []); if (loading) return <ActivityIndicator />; if (error) return <Text>Error: {error}</Text>; return ( <FlatList data={products} keyExtractor={(p) => String(p.id)} renderItem={({ item }) => ( <View> <Text>{item.name}</Text> <Text>${item.price.toFixed(2)}</Text> </View> )} /> ); }
2. React Native — Session Store via WeftKitMem
Rather than connecting directly to Redis from mobile, the backend uses WeftKitMem as a session store, and the mobile app interacts with session-aware API endpoints.
typescript// Backend pattern (Node.js + express-session + ioredis) // This runs on YOUR server, not in the mobile app // import session from 'express-session'; // import { RedisStore } from 'connect-redis'; // import Redis from 'ioredis'; // // const client = new Redis({ host: 'localhost', port: 6379, password: process.env.REDIS_PASS }); // app.use(session({ store: new RedisStore({ client }), secret: process.env.SESSION_SECRET }));
typescript// Mobile: exchange credentials for a short-lived JWT backed by WeftKitMem session export async function refreshSession(): Promise<string> { const refreshToken = await SecureStore.getItemAsync('refresh_token'); if (!refreshToken) throw new Error('No refresh token'); const { data } = await apiClient.post<{ access_token: string }>('/auth/refresh', { refresh_token: refreshToken, }); return data.access_token; }
3. React Native — Offline-First with SQLite + Sync
For apps that need to work without connectivity, use expo-sqlite for local storage and sync with WeftKitRel via your backend API.
bashnpx expo install expo-sqlite
typescript// src/db/local.ts import * as SQLite from 'expo-sqlite'; const db = SQLite.openDatabaseSync('myapp.db'); // Create local schema db.execSync(` CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL NOT NULL, synced_at INTEGER, dirty INTEGER DEFAULT 0 ); `); export function upsertProduct(product: { id: number; name: string; price: number }): void { db.runSync( 'INSERT OR REPLACE INTO products (id, name, price, dirty) VALUES (?, ?, ?, 0)', product.id, product.name, product.price ); } export function getUnsyncedProducts(): { id: number; name: string; price: number }[] { return db.getAllSync('SELECT * FROM products WHERE dirty = 1'); }
typescript// src/sync/sync-engine.ts import { getUnsyncedProducts } from '../db/local'; import { apiClient } from '../api/axios-client'; export async function syncToBackend(): Promise<void> { const dirty = getUnsyncedProducts(); if (dirty.length === 0) return; // Your backend receives these and writes to WeftKitRel await apiClient.post('/sync/products', { products: dirty }); // Mark as synced locally // db.runSync('UPDATE products SET dirty = 0 WHERE dirty = 1'); }
Conflict resolution strategies:
| Strategy | When to use | |---|---| | Last-write-wins (timestamp) | Simple data, low contention | | Server-wins | Read-heavy apps; server is source of truth | | Three-way merge | Collaborative editing, shared documents | | Operational transforms | Real-time collaborative features |
4. Flutter — Calling a WeftKit-backed REST API
yaml# pubspec.yaml dependencies: flutter: sdk: flutter http: ^1.2.2 dio: ^5.5.4 flutter_secure_storage: ^9.2.2
API Service Class
dart// lib/services/api_service.dart import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; class ApiService { static const String _baseUrl = String.fromEnvironment( 'API_BASE_URL', defaultValue: 'https://api.example.com', ); final Dio _dio; final FlutterSecureStorage _storage; ApiService({FlutterSecureStorage? storage}) : _storage = storage ?? const FlutterSecureStorage(), _dio = Dio(BaseOptions( baseUrl: _baseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), headers: {'Content-Type': 'application/json'}, )) { _dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { final token = await _storage.read(key: 'access_token'); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } handler.next(options); }, onError: (error, handler) async { if (error.response?.statusCode == 401) { await _storage.delete(key: 'access_token'); // Trigger re-authentication via your app's auth flow } handler.next(error); }, )); } Future<List<Product>> listProducts({int page = 1, int perPage = 20}) async { try { final response = await _dio.get<Map<String, dynamic>>( '/products', queryParameters: {'page': page, 'per_page': perPage}, ); final items = response.data!['data'] as List<dynamic>; return items.map((json) => Product.fromJson(json as Map<String, dynamic>)).toList(); } on DioException catch (e) { throw ApiException.fromDioException(e); } } Future<User> login(String email, String password) async { try { final response = await _dio.post<Map<String, dynamic>>( '/auth/login', data: {'email': email, 'password': password}, ); final token = response.data!['token'] as String; await _storage.write(key: 'access_token', value: token); return User.fromJson(response.data!['user'] as Map<String, dynamic>); } on DioException catch (e) { throw ApiException.fromDioException(e); } } }
Dart Model Class
dart// lib/models/product.dart class Product { final int id; final String name; final String description; final double price; final bool inStock; final String? imageUrl; const Product({ required this.id, required this.name, required this.description, required this.price, required this.inStock, this.imageUrl, }); factory Product.fromJson(Map<String, dynamic> json) { return Product( id: json['id'] as int, name: json['name'] as String, description: json['description'] as String, price: (json['price'] as num).toDouble(), inStock: json['in_stock'] as bool, imageUrl: json['image_url'] as String?, ); } Map<String, dynamic> toJson() => { 'id': id, 'name': name, 'description': description, 'price': price, 'in_stock': inStock, 'image_url': imageUrl, }; } class User { final int id; final String name; final String email; const User({required this.id, required this.name, required this.email}); factory User.fromJson(Map<String, dynamic> json) => User( id: json['id'] as int, name: json['name'] as String, email: json['email'] as String, ); } class ApiException implements Exception { final int statusCode; final String message; const ApiException({required this.statusCode, required this.message}); factory ApiException.fromDioException(DioException e) { return ApiException( statusCode: e.response?.statusCode ?? 0, message: (e.response?.data as Map<String, dynamic>?)?['message']?.toString() ?? e.message ?? 'Unknown error', ); } @override String toString() => 'ApiException($statusCode): $message'; }
Flutter Widget
dart// lib/screens/product_list_screen.dart import 'package:flutter/material.dart'; import '../models/product.dart'; import '../services/api_service.dart'; class ProductListScreen extends StatefulWidget { const ProductListScreen({super.key}); @override State<ProductListScreen> createState() => _ProductListScreenState(); } class _ProductListScreenState extends State<ProductListScreen> { final ApiService _api = ApiService(); late Future<List<Product>> _productsFuture; @override void initState() { super.initState(); _productsFuture = _api.listProducts(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Products')), body: FutureBuilder<List<Product>>( future: _productsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}')); } final products = snapshot.data!; return ListView.builder( itemCount: products.length, itemBuilder: (context, index) { final p = products[index]; return ListTile( title: Text(p.name), subtitle: Text('\$${p.price.toStringAsFixed(2)}'), trailing: p.inStock ? const Icon(Icons.check_circle, color: Colors.green) : const Icon(Icons.cancel, color: Colors.red), ); }, ); }, ), ); } }
5. Flutter — Direct PostgreSQL via postgres Dart Package
Important: Use this only for server-side Dart (Dart CLI tools, backend services, or Flutter desktop apps). Do not use a direct PostgreSQL connection in an iOS or Android mobile app — doing so exposes credentials and requires port 5432 to be publicly accessible.
yaml# pubspec.yaml (server-side Dart or Flutter desktop only) dependencies: postgres: ^3.4.0
dartimport 'package:postgres/postgres.dart'; Future<void> main() async { final connection = await Connection.open( Endpoint( host: 'localhost', port: 5432, database: 'mydb', username: 'app_user', password: 'secret', ), settings: ConnectionSettings(sslMode: SslMode.require), ); // Query final result = await connection.execute( Sql.named('SELECT id, name, price FROM products WHERE active = @active ORDER BY name'), parameters: {'active': true}, ); for (final row in result) { print('${row[1]} — \$${row[2]}'); } // Insert with RETURNING final inserted = await connection.execute( Sql.named( 'INSERT INTO products (name, price) VALUES (@name, @price) RETURNING id' ), parameters: {'name': 'Widget Pro', 'price': 29.99}, ); print('New product ID: ${inserted.first[0]}'); await connection.close(); }
6. Flutter — MongoDB via mongo_dart
Important: Use
mongo_dartonly for server-side Dart (Dart CLI tools, backend services, or Flutter desktop apps). Do not use direct MongoDB connections in iOS or Android mobile apps.
yaml# pubspec.yaml (server-side Dart or Flutter desktop only) dependencies: mongo_dart: ^0.10.4
dartimport 'package:mongo_dart/mongo_dart.dart'; Future<void> main() async { final db = await Db.create('mongodb://app_user:secret@localhost:27017/mydb'); await db.open(); final collection = db.collection('articles'); // Insert await collection.insertOne({ 'title': 'Getting Started', 'author': 'Alice', 'tags': ['database', 'weftkit'], 'views': 0, }); // Find one final article = await collection.findOne(where.eq('author', 'Alice')); print(article?['title']); // Find many final cursor = collection.find(where.gt('views', 100).sortBy('views', descending: true)); await for (final doc in cursor) { print('${doc['title']} — ${doc['views']} views'); } // Update await collection.updateOne( where.eq('title', 'Getting Started'), modify.set('views', 1500).set('featured', true), ); await db.close(); }
7. Security Recommendations for Mobile
Following these practices is essential when your mobile app communicates with a WeftKit-backed API.
Never Hard-Code Credentials
dart// WRONG — credentials extractable from the APK/IPA // const dbPassword = 'secret'; // const apiKey = 'sk-live-abc123'; // CORRECT — use environment variables injected at build time const apiBaseUrl = String.fromEnvironment('API_BASE_URL');
typescript// React Native — use Expo config or react-native-config // Never commit .env files with real secrets to version control const API_URL = process.env.EXPO_PUBLIC_API_URL;
Use Secure Token Storage
dart// Flutter — flutter_secure_storage uses Keychain (iOS) and EncryptedSharedPreferences (Android) import 'package:flutter_secure_storage/flutter_secure_storage.dart'; final storage = FlutterSecureStorage( aOptions: AndroidOptions(encryptedSharedPreferences: true), iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device), ); await storage.write(key: 'access_token', value: token); final token = await storage.read(key: 'access_token');
typescript// React Native — use expo-secure-store import * as SecureStore from 'expo-secure-store'; await SecureStore.setItemAsync('access_token', token); const token = await SecureStore.getItemAsync('access_token'); // Never use AsyncStorage for tokens — it is unencrypted // ❌ await AsyncStorage.setItem('token', value);
Use Short-Lived JWT Tokens
Access token: 15 minutes — used for API calls
Refresh token: 30 days — stored in secure storage, used only to get new access tokens
typescript// Automatically refresh expired tokens using an axios interceptor client.interceptors.response.use( (response) => response, async (error) => { const original = error.config; if (error.response?.status === 401 && !original._retry) { original._retry = true; const newToken = await refreshSession(); original.headers.Authorization = `Bearer ${newToken}`; return client(original); } return Promise.reject(error); } );
Certificate Pinning
Pin your API server's certificate to prevent MITM attacks:
dart// Flutter — using dio with certificate pinning import 'dart:io'; import 'package:dio/dio.dart'; HttpClient buildPinnedClient(List<String> allowedSha256Fingerprints) { return HttpClient() ..badCertificateCallback = (cert, host, port) { final fingerprint = cert.der .fold<int>(0, (prev, b) => prev + b) // simplified — use a proper SHA256 check .toRadixString(16); return allowedSha256Fingerprints.contains(fingerprint); }; }
typescript// React Native — use react-native-ssl-pinning import { fetch } from 'react-native-ssl-pinning'; const response = await fetch('https://api.example.com/products', { method: 'GET', sslPinning: { certs: ['api_cert_sha256_hash'], // SHA-256 hash of the server certificate }, headers: { Authorization: `Bearer ${token}` }, });
Use Environment-Specific API URLs
| Environment | API Base URL |
|---|---|
| Development | http://localhost:3000 |
| Staging | https://api-staging.example.com |
| Production | https://api.example.com |
Set the URL at build time rather than at runtime:
bash# Expo (React Native) EXPO_PUBLIC_API_URL=https://api.example.com npx expo build # Flutter flutter build apk --dart-define=API_BASE_URL=https://api.example.com
Summary
| Approach | React Native | Flutter |
|---|---|---|
| Recommended: REST API backend | fetch / axios | http / dio |
| Offline-first local storage | expo-sqlite | sqflite / drift |
| Secure token storage | expo-secure-store | flutter_secure_storage |
| Direct DB (server-side Dart only) | N/A | postgres / mongo_dart |
Next Steps
- Deployment — Start and configure WeftKit Standalone
- Security — Configure TLS, JWT, and mTLS for production
- Node.js Integration — Build your API backend
- Integration Guides — Other language guides
On this page