Skip to main content

Bridge API

The bridge is the communication layer between mini-apps (running in WebViews/JS containers) and the host app (React Native). It handles request/response messaging, event subscriptions, and service routing.

Architecture Overview

┌─────────────────────────────────────────────────────────────────┐
│ Mini-App (SDK) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ Hooks │ │ Services │ │ Components │ │
│ │ useHostAuth │ │ storage │ │ PaymentButton │ │
│ │ useHostPush │ │ push │ │ ShareButton │ │
│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │
│ │ │ │ │
│ └────────────────┼─────────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Bridge │ │
│ │ (SDK side) │ │
│ └──────┬──────┘ │
└───────────────────────────┼─────────────────────────────────────┘

┌───────┴───────┐
│ Native Bridge │
│ (postMessage) │
└───────┬───────┘

┌───────────────────────────┼─────────────────────────────────────┐
│ ▼ │
│ ┌────────────────┐ │
│ │ ServiceRouter │ │
│ │ (Runtime side) │ │
│ └───────┬────────┘ │
│ │ │
│ ┌────────────────────┼────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Auth │ │ Storage │ │ Push │ │
│ │Handler │ │ Handler │ │ Handler │ │
│ └────────┘ └──────────┘ └──────────┘ │
│ │
│ Host App (Runtime) │
└─────────────────────────────────────────────────────────────────┘

Bridge (SDK Side)

The Bridge class manages communication from the mini-app to the host.

TypeScript Interface

type BridgeMessageType = 'request' | 'response' | 'event';

interface BridgeMessage {
/** Unique message identifier */
id: string;
/** Message type */
type: BridgeMessageType;
/** Action/method name (e.g., 'storage.get') */
action: string;
/** Message payload */
payload?: unknown;
/** Timestamp when message was created */
timestamp: number;
}

interface BridgeRequest extends BridgeMessage {
type: 'request';
}

interface BridgeResponse extends BridgeMessage {
type: 'response';
/** Whether the request succeeded */
success: boolean;
/** Response data (if success) */
data?: unknown;
/** Error message (if failed) */
error?: string;
}

interface BridgeEvent extends BridgeMessage {
type: 'event';
}

interface Bridge {
/**
* Initialize the bridge with mini-app ID
*/
initialize(appId: string): void;

/**
* Wait for bridge to be ready
*/
waitForReady(): Promise<void>;

/**
* Send a request and wait for response
* @param action - The action name (e.g., 'storage.get')
* @param payload - Request payload
* @returns Promise resolving to response data
*/
send<T = unknown>(action: string, payload?: unknown): Promise<T>;

/**
* Subscribe to events from the host
* @param eventName - Event to listen for
* @param callback - Handler function
* @returns Unsubscribe function
*/
on(eventName: string, callback: (event: BridgeEvent) => void): () => void;

/**
* Emit a one-way event (no response expected)
* @param action - Event action name
* @param payload - Event payload
*/
emit(action: string, payload?: unknown): void;
}

Example Usage

import { bridge } from '@54vie/miniapp-sdk/bridge';

// Initialize bridge (usually done by MiniAppRoot)
bridge.initialize('com.example.myapp');

// Wait for bridge to be ready
await bridge.waitForReady();

// Send request and get response
const user = await bridge.send<User>('auth.getUser');
console.log('User:', user);

// Send request with payload
const value = await bridge.send<string>('storage.get', { key: 'theme' });

// Listen for events from host
const unsubscribe = bridge.on('lifecycle.suspend', (event) => {
console.log('App suspended at:', event.timestamp);
saveState();
});

// Emit event (fire-and-forget)
bridge.emit('analytics.track', {
eventName: 'button_clicked',
properties: { button: 'submit' },
});

// Cleanup
unsubscribe();

ServiceRouter (Runtime Side)

The ServiceRouter routes incoming bridge requests to appropriate service handlers in the host app.

TypeScript Interface

type ServiceCategory =
| 'auth'
| 'wallet'
| 'ui'
| 'navigation'
| 'storage'
| 'media'
| 'location'
| 'share'
| 'analytics'
| 'host'
| 'device'
| 'network'
| 'haptics'
| 'clipboard'
| 'push'
| 'biometric'
| 'api';

interface BridgeRequest {
/** Unique request identifier */
id: string;
/** Mini-app ID making the request */
appId: string;
/** Method name (e.g., 'storage.get') */
method: string;
/** Request parameters */
params?: Record<string, unknown>;
/** Request timestamp */
timestamp: number;
}

interface BridgeResponse {
/** Request ID this responds to */
id: string;
/** Whether request succeeded */
success: boolean;
/** Response data */
data?: unknown;
/** Error details */
error?: {
code: string;
message: string;
details?: unknown;
};
}

interface BridgeError {
code: string;
message: string;
details?: unknown;
}

type BridgeHandler = (
params: Record<string, unknown>,
request?: BridgeRequest
) => Promise<unknown>;

interface ServiceHandler {
category: ServiceCategory;
methods: Record<string, BridgeHandler>;
}

interface ServiceRouter {
/**
* Register a service handler
*/
registerService(handler: ServiceHandler): void;

/**
* Register methods for a category
*/
register(category: ServiceCategory, methods: Record<string, BridgeHandler>): void;

/**
* Unregister a service
*/
unregisterService(category: ServiceCategory): void;

/**
* Route a request to appropriate handler
*/
route(request: BridgeRequest): Promise<BridgeResponse>;

/**
* Get all registered service categories
*/
getRegisteredServices(): ServiceCategory[];

/**
* Get all registered method names
*/
getRegisteredMethods(): string[];

/**
* Check if a method is registered
*/
hasMethod(method: string): boolean;
}

Example: Registering Service Handlers

import { serviceRouter } from '@54vie/miniapp-runtime';

// Register storage service
serviceRouter.register('storage', {
async get(params) {
const { key, scope } = params as { key: string; scope: string };
return AsyncStorage.getItem(`${scope}:${key}`);
},

async set(params) {
const { key, value, scope } = params as { key: string; value: unknown; scope: string };
await AsyncStorage.setItem(`${scope}:${key}`, JSON.stringify(value));
},

async remove(params) {
const { key, scope } = params as { key: string; scope: string };
await AsyncStorage.removeItem(`${scope}:${key}`);
},

async clear(params) {
const { scope } = params as { scope: string };
const keys = await AsyncStorage.getAllKeys();
const scopedKeys = keys.filter(k => k.startsWith(`${scope}:`));
await AsyncStorage.multiRemove(scopedKeys);
},
});

// Register auth service
serviceRouter.register('auth', {
async getUser(params, request) {
// request contains appId for permission checking
const user = await authStore.getUser();
return {
id: user.id,
name: user.name,
avatar: user.avatar,
};
},

async getAccessToken() {
return authStore.getAccessToken();
},

async isAuthenticated() {
return authStore.isAuthenticated();
},
});

// Register using ServiceHandler object
serviceRouter.registerService({
category: 'push',
methods: {
async getToken() {
return messaging().getToken();
},
async hasPermission() {
const status = await messaging().hasPermission();
return status === messaging.AuthorizationStatus.AUTHORIZED;
},
async requestPermission() {
const status = await messaging().requestPermission();
return status === messaging.AuthorizationStatus.AUTHORIZED;
},
},
});

Request/Response Format

Request Format

// Request sent from mini-app to host
{
id: "com.example.app-1-1706601234567",
type: "request",
action: "storage.get",
payload: {
appId: "com.example.app",
key: "user_preferences"
},
timestamp: 1706601234567
}

Response Format

// Successful response
{
id: "com.example.app-1-1706601234567",
type: "response",
success: true,
data: {
theme: "dark",
language: "vi"
},
timestamp: 1706601234600
}

// Error response
{
id: "com.example.app-1-1706601234567",
type: "response",
success: false,
error: {
code: "PERMISSION_DENIED",
message: "Storage permission not granted"
},
timestamp: 1706601234600
}

Event Format

// Event from mini-app to host (fire-and-forget)
{
id: "com.example.app-2-1706601235000",
type: "event",
action: "analytics.track",
payload: {
appId: "com.example.app",
eventName: "purchase_completed",
properties: {
orderId: "ORD-123",
amount: 500000
}
},
timestamp: 1706601235000
}

// Event from host to mini-app
{
id: "host-event-1706601236000",
type: "event",
action: "lifecycle.suspend",
payload: {
reason: "app_backgrounded"
},
timestamp: 1706601236000
}

Available Bridge Methods

Authentication (auth.*)

MethodParametersReturnsDescription
auth.getUser-UserGet current user
auth.getAccessToken-stringGet JWT token
auth.isAuthenticated-booleanCheck auth status
auth.requestLogin-booleanRequest user login
auth.checkPermission{ permission }booleanCheck if permission granted
auth.requestPermission{ permission, reason? }booleanRequest permission

Wallet (wallet.*)

MethodParametersReturnsDescription
wallet.getBalance-numberGet wallet balance
wallet.requestPayment{ amount, description, orderId?, metadata? }PaymentResultRequest payment
wallet.checkTransaction{ transactionId }TransactionCheck transaction status

Storage (storage.*)

MethodParametersReturnsDescription
storage.get{ key, scope }T | nullGet stored value
storage.set{ key, value, scope }voidSet value
storage.remove{ key, scope }voidRemove value
storage.clear{ scope }voidClear all storage
MethodParametersReturnsDescription
navigation.goBack-voidGo back
navigation.navigate{ route, params? }voidNavigate to route
navigation.setTitle{ title }voidSet screen title
navigation.setOptions{ options }voidSet nav options
navigation.openMiniApp{ appId, params? }voidOpen another mini-app
navigation.openDeepLink{ url }voidOpen deep link
navigation.exitMiniApp-voidExit mini-app

UI (ui.*)

MethodParametersReturnsDescription
ui.showToast{ message, type?, duration? }voidShow toast
ui.showAlert{ title, message, confirmText?, cancelText? }booleanShow alert dialog
ui.showActionSheet{ title?, options, cancelIndex? }numberShow action sheet
ui.showLoading-voidShow loading overlay
ui.hideLoading-voidHide loading overlay

Device (device.*)

MethodParametersReturnsDescription
device.getInfo-DeviceInfoGet device info
device.getDeviceId-stringGet device ID
device.getAppVersion-stringGet app version
device.getBuildNumber-stringGet build number
device.isEmulator-booleanCheck if emulator
device.getCarrier-string | nullGet carrier name

Network (network.*)

MethodParametersReturnsDescription
network.getState-NetworkStateGet network state
network.isOnline-booleanCheck if online
network.getConnectionType-ConnectionTypeGet connection type
network.isWifi-booleanCheck if WiFi
network.isCellular-booleanCheck if cellular

Push Notifications (push.*)

MethodParametersReturnsDescription
push.getToken-string | nullGet FCM/APNs token
push.hasPermission-booleanCheck permission
push.requestPermission-booleanRequest permission
push.subscribeToTopic{ topic }voidSubscribe to topic
push.unsubscribeFromTopic{ topic }voidUnsubscribe from topic
push.scheduleNotification{ notification }stringSchedule notification
push.cancelNotification{ notificationId }voidCancel notification
push.getBadgeCount-numberGet badge count
push.setBadgeCount{ count }voidSet badge count

Analytics (analytics.*)

MethodParametersReturnsDescription
analytics.track{ eventName, properties? }voidTrack event
analytics.screen{ screenName }voidTrack screen view

Media (media.*)

MethodParametersReturnsDescription
media.takePhoto{ options? }MediaAssetTake photo
media.recordVideo{ options? }MediaAssetRecord video
media.pickImage{ options? }MediaAssetPick from gallery
media.pickMultipleImages{ options? }MediaAsset[]Pick multiple
media.pickVideo-MediaAssetPick video
media.resizeImage{ uri, options }MediaAssetResize image
media.compressImage{ uri, quality }MediaAssetCompress image
media.cropImage{ uri, options }MediaAssetCrop image
media.scanQR-stringScan QR code

Location (location.*)

MethodParametersReturnsDescription
location.getCurrentPosition{ options? }LocationGet current location
location.checkPermission-PermissionStatusCheck permission
location.requestPermission-PermissionStatusRequest permission
location.geocode{ address }LocationAddress to coordinates
location.reverseGeocode{ latitude, longitude }AddressCoordinates to address
location.searchPlaces{ query, location? }PlaceResult[]Search places
location.watchPosition{ callback }voidWatch position
location.stopWatchPosition-voidStop watching

Share (share.*)

MethodParametersReturnsDescription
share.text{ text }voidShare text
share.image{ imageUri }voidShare image
share.toChat{ content }voidShare to chat

Biometric (biometric.*)

MethodParametersReturnsDescription
biometric.isSupported-booleanCheck if supported
biometric.isEnabled-booleanCheck if enabled
biometric.authenticate{ reason }booleanAuthenticate user

API Proxy (api.*)

MethodParametersReturnsDescription
api.request{ url, method?, headers?, body?, timeout? }ApiResponseMake API request

Lifecycle (lifecycle.*)

MethodParametersReturnsDescription
lifecycle.ready-voidSignal mini-app is ready
lifecycle.suspend-voidRequest suspend
lifecycle.resume-voidRequest resume
lifecycle.destroy-voidRequest destroy

Error Codes

CodeDescription
METHOD_NOT_FOUNDThe requested method does not exist
HANDLER_ERRORHandler threw an exception
PERMISSION_DENIEDMini-app lacks required permission
INVALID_PARAMSInvalid or missing parameters
TIMEOUTRequest timed out (30s default)
NETWORK_ERRORNetwork request failed
AUTH_REQUIREDUser must be authenticated
RATE_LIMITEDToo many requests

Best Practices

  1. Use hooks/services instead of direct bridge calls - The SDK hooks and services provide better abstractions with automatic cleanup and error handling.

  2. Handle errors gracefully - Always wrap bridge calls in try/catch and handle specific error codes.

  3. Respect timeouts - Bridge requests timeout after 30 seconds. Long-running operations should use different patterns.

  4. Clean up subscriptions - Always unsubscribe from events when components unmount.

useEffect(() => {
const unsubscribe = bridge.on('lifecycle.suspend', handleSuspend);
return () => unsubscribe();
}, []);
  1. Check permissions before sensitive operations - Use auth.checkPermission before accessing sensitive data.

For more information on the higher-level APIs, see the Hooks API and Services API documentation.