Skip to content
WeftKitBeta

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

bash
npm 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.

bash
npx 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
dart
import '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_dart only 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
dart
import '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