# RevoLand SuperApp - Runtime Modularization Architecture with Security ## 1. Project Overview **RevoLand** is a real estate SuperApp built on Flutter (iOS & Android) that dynamically loads and runs SubApps remotely through WebView at runtime. Each SubApp resides in its own repository, deployed as Flutter Web, and hosted on a server or CDN. This allows updates without republishing the main SuperApp, with robust security mechanisms to prevent unauthorized access. ## 2. Architecture Components ### 2.1 SuperApp (Flutter Native) The main RevoLand app contains the core interface, navigation, and shared services. **Key Responsibilities:** - User authentication - Fetch available SubApps from Metadata API - Display dynamic menu based on user permissions - Launch SubApps in WebView with secure context - Establish bidirectional communication bridge **Example Implementation:** ```dart // lib/models/sub_app.dart class SubApp { final String id; final String displayName; final String description; final String iconUrl; final String launchUrl; final String signedToken; final List permissions; SubApp({ required this.id, required this.displayName, required this.description, required this.iconUrl, required this.launchUrl, required this.signedToken, required this.permissions, }); factory SubApp.fromJson(Map json) { return SubApp( id: json['id'] as String, displayName: json['displayName'] as String, description: json['description'] as String, iconUrl: json['iconUrl'] as String, launchUrl: json['launchUrl'] as String, signedToken: json['signedToken'] as String, permissions: List.from(json['permissions'] ?? []), ); } } ``` ```dart // lib/services/metadata_service.dart import 'package:dio/dio.dart'; import '../models/sub_app.dart'; class MetadataService { final Dio _dio; final String _baseUrl; MetadataService({ required String baseUrl, required String authToken, }) : _baseUrl = baseUrl, _dio = Dio(BaseOptions( baseUrl: baseUrl, headers: { 'Authorization': 'Bearer $authToken', 'Content-Type': 'application/json', }, )); Future> fetchAvailableSubApps() async { try { final response = await _dio.get('/api/v1/subapps/available'); if (response.statusCode == 200) { final List data = response.data['subApps']; return data.map((json) => SubApp.fromJson(json)).toList(); } else { throw Exception('Failed to fetch SubApps'); } } catch (e) { throw Exception('Error fetching SubApps: $e'); } } Future refreshSubAppToken(String subAppId) async { try { final response = await _dio.post( '/api/v1/subapps/$subAppId/token/refresh', ); if (response.statusCode == 200) { return response.data['signedToken'] as String; } else { throw Exception('Failed to refresh token'); } } catch (e) { throw Exception('Error refreshing token: $e'); } } } ``` ```dart // lib/screens/sub_app_launcher.dart import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../models/sub_app.dart'; class SubAppLauncher extends StatefulWidget { final SubApp subApp; final String userId; final String userRole; const SubAppLauncher({ Key? key, required this.subApp, required this.userId, required this.userRole, }) : super(key: key); @override State createState() => _SubAppLauncherState(); } class _SubAppLauncherState extends State { late WebViewController _controller; bool _isLoading = true; @override void initState() { super.initState(); _initializeWebView(); } void _initializeWebView() { _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setBackgroundColor(Colors.white) ..addJavaScriptChannel( 'RevoLandNativeBridge', onMessageReceived: _handleNativeMessage, ) ..setNavigationDelegate( NavigationDelegate( onPageStarted: (url) { setState(() => _isLoading = true); }, onPageFinished: (url) { _injectSuperAppContext(); setState(() => _isLoading = false); }, onWebResourceError: (error) { debugPrint('WebView error: ${error.description}'); }, ), ) ..loadRequest(Uri.parse( '${widget.subApp.launchUrl}?token=${widget.subApp.signedToken}&hostAppId=revoland-mobile', )); } void _injectSuperAppContext() { final context = ''' window.__REVOLAND_CONTEXT__ = { token: "${widget.subApp.signedToken}", hostAppId: "revoland-mobile", platform: "${Theme.of(context).platform.name}", userId: "${widget.userId}", userRole: "${widget.userRole}", subAppId: "${widget.subApp.id}", theme: { primaryColor: "#2563eb", isDarkMode: ${Theme.of(context).brightness == Brightness.dark} }, locale: "${Localizations.localeOf(context).languageCode}" }; if (window.onRevoLandContextReady) { window.onRevoLandContextReady(window.__REVOLAND_CONTEXT__); } console.log("RevoLand context injected:", window.__REVOLAND_CONTEXT__); '''; _controller.runJavaScript(context); } void _handleNativeMessage(JavaScriptMessage message) { debugPrint('Message from SubApp: ${message.message}'); try { final data = jsonDecode(message.message); final action = data['action'] as String?; switch (action) { case 'subappReady': debugPrint('SubApp ${widget.subApp.id} is ready'); break; case 'requestLocation': _handleLocationRequest(data); break; case 'shareProperty': _handleShareProperty(data); break; case 'openMap': _handleOpenMap(data); break; case 'navigateBack': Navigator.of(context).pop(); break; default: debugPrint('Unknown action: $action'); } } catch (e) { debugPrint('Error handling message: $e'); } } void _handleLocationRequest(Map data) { // Request device location and send back to SubApp // Implementation depends on location service } void _handleShareProperty(Map data) { final propertyId = data['propertyId'] as String?; final propertyUrl = data['propertyUrl'] as String?; // Trigger native share dialog } void _handleOpenMap(Map data) { final lat = data['latitude'] as double?; final lng = data['longitude'] as double?; // Open native maps app } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.subApp.displayName), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: () => _controller.reload(), ), ], ), body: Stack( children: [ WebViewWidget(controller: _controller), if (_isLoading) const Center( child: CircularProgressIndicator(), ), ], ), ); } } ``` ```dart // lib/screens/home_screen.dart import 'package:flutter/material.dart'; import '../models/sub_app.dart'; import '../services/metadata_service.dart'; import 'sub_app_launcher.dart'; class HomeScreen extends StatefulWidget { final String userId; final String userRole; final String authToken; const HomeScreen({ Key? key, required this.userId, required this.userRole, required this.authToken, }) : super(key: key); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { late MetadataService _metadataService; List _subApps = []; bool _isLoading = true; String? _error; @override void initState() { super.initState(); _metadataService = MetadataService( baseUrl: 'https://api.revoland.com', authToken: widget.authToken, ); _loadSubApps(); } Future _loadSubApps() async { setState(() { _isLoading = true; _error = null; }); try { final subApps = await _metadataService.fetchAvailableSubApps(); setState(() { _subApps = subApps; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } void _launchSubApp(SubApp subApp) { Navigator.push( context, MaterialPageRoute( builder: (context) => SubAppLauncher( subApp: subApp, userId: widget.userId, userRole: widget.userRole, ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('RevoLand'), actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loadSubApps, ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center(child: Text('Error: $_error')) : GridView.builder( padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, mainAxisSpacing: 16, childAspectRatio: 1.2, ), itemCount: _subApps.length, itemBuilder: (context, index) { final subApp = _subApps[index]; return _SubAppCard( subApp: subApp, onTap: () => _launchSubApp(subApp), ); }, ), ); } } class _SubAppCard extends StatelessWidget { final SubApp subApp; final VoidCallback onTap; const _SubAppCard({ required this.subApp, required this.onTap, }); @override Widget build(BuildContext context) { return Card( elevation: 2, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.network( subApp.iconUrl, width: 48, height: 48, errorBuilder: (context, error, stackTrace) => const Icon(Icons.apps, size: 48), ), const SizedBox(height: 12), Text( subApp.displayName, style: Theme.of(context).textTheme.titleMedium, textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( subApp.description, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ), ), ); } } ``` ### 2.2 Metadata API (Backend) Backend service that manages SubApp metadata and authentication. **Example Implementation (Node.js + Express):** ```typescript // src/types/subapp.types.ts export interface SubAppMetadata { id: string; displayName: string; description: string; iconUrl: string; launchUrl: string; signedToken: string; permissions: string[]; version: string; minHostVersion: string; } export interface JWTPayload { userId: string; hostAppId: string; subAppId: string; userRole: string; permissions: string[]; iat: number; exp: number; } ``` ```typescript // src/middleware/auth.middleware.ts import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; export interface AuthRequest extends Request { user?: { userId: string; role: string; email: string; }; } export const authenticateUser = ( req: AuthRequest, res: Response, next: NextFunction ): void => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { res.status(401).json({ error: 'No token provided' }); return; } const token = authHeader.substring(7); const decoded = jwt.verify(token, JWT_SECRET) as { userId: string; role: string; email: string; }; req.user = decoded; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } }; ``` ```typescript // src/services/subapp.service.ts import jwt from 'jsonwebtoken'; import { SubAppMetadata, JWTPayload } from '../types/subapp.types'; const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; const TOKEN_EXPIRY = '5m'; // 5 minutes export class SubAppService { private subAppRegistry: Map> = new Map([ [ 'property-listings', { id: 'property-listings', displayName: 'Property Listings', description: 'Browse available properties', iconUrl: 'https://cdn.revoland.com/icons/listings.png', launchUrl: 'https://cdn.revoland.com/subapps/property-listings/index.html', permissions: ['read:properties', 'search:properties'], version: '1.2.0', minHostVersion: '1.0.0', }, ], [ 'virtual-tours', { id: 'virtual-tours', displayName: 'Virtual Tours', description: '360° property tours', iconUrl: 'https://cdn.revoland.com/icons/virtual-tour.png', launchUrl: 'https://cdn.revoland.com/subapps/virtual-tours/index.html', permissions: ['read:properties', 'view:tours'], version: '2.0.1', minHostVersion: '1.0.0', }, ], [ 'mortgage-calculator', { id: 'mortgage-calculator', displayName: 'Mortgage Calculator', description: 'Calculate mortgage payments', iconUrl: 'https://cdn.revoland.com/icons/calculator.png', launchUrl: 'https://cdn.revoland.com/subapps/mortgage-calculator/index.html', permissions: ['calculate:mortgage'], version: '1.5.2', minHostVersion: '1.0.0', }, ], [ 'agent-dashboard', { id: 'agent-dashboard', displayName: 'Agent Dashboard', description: 'Manage your properties and leads', iconUrl: 'https://cdn.revoland.com/icons/dashboard.png', launchUrl: 'https://cdn.revoland.com/subapps/agent-dashboard/index.html', permissions: ['manage:properties', 'manage:leads', 'view:analytics'], version: '3.1.0', minHostVersion: '1.0.0', }, ], ]); private rolePermissions: Record = { buyer: ['read:properties', 'search:properties', 'view:tours', 'calculate:mortgage'], seller: ['read:properties', 'search:properties', 'view:tours'], agent: ['read:properties', 'search:properties', 'view:tours', 'calculate:mortgage', 'manage:properties', 'manage:leads', 'view:analytics'], admin: ['*'], }; generateSubAppToken( userId: string, subAppId: string, userRole: string, permissions: string[] ): string { const payload: JWTPayload = { userId, hostAppId: 'revoland-mobile', subAppId, userRole, permissions, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes }; return jwt.sign(payload, JWT_SECRET); } getUserPermissions(userRole: string): string[] { return this.rolePermissions[userRole] || []; } hasPermission(userPermissions: string[], requiredPermission: string): boolean { if (userPermissions.includes('*')) return true; return userPermissions.includes(requiredPermission); } getAvailableSubApps(userId: string, userRole: string): SubAppMetadata[] { const userPermissions = this.getUserPermissions(userRole); const availableSubApps: SubAppMetadata[] = []; for (const [id, subApp] of this.subAppRegistry) { // Check if user has at least one required permission const hasAccess = subApp.permissions.some((perm) => this.hasPermission(userPermissions, perm) ); if (hasAccess) { const signedToken = this.generateSubAppToken( userId, id, userRole, userPermissions.filter((p) => subApp.permissions.includes(p) || p === '*') ); availableSubApps.push({ ...subApp, signedToken, }); } } return availableSubApps; } refreshToken(userId: string, subAppId: string, userRole: string): string | null { const subApp = this.subAppRegistry.get(subAppId); if (!subApp) return null; const userPermissions = this.getUserPermissions(userRole); const hasAccess = subApp.permissions.some((perm) => this.hasPermission(userPermissions, perm) ); if (!hasAccess) return null; return this.generateSubAppToken( userId, subAppId, userRole, userPermissions.filter((p) => subApp.permissions.includes(p) || p === '*') ); } } ``` ```typescript // src/routes/subapp.routes.ts import { Router } from 'express'; import { AuthRequest, authenticateUser } from '../middleware/auth.middleware'; import { SubAppService } from '../services/subapp.service'; const router = Router(); const subAppService = new SubAppService(); // Get available SubApps for current user router.get('/available', authenticateUser, (req: AuthRequest, res) => { try { if (!req.user) { res.status(401).json({ error: 'Unauthorized' }); return; } const subApps = subAppService.getAvailableSubApps( req.user.userId, req.user.role ); res.json({ success: true, subApps, timestamp: new Date().toISOString(), }); } catch (error) { console.error('Error fetching SubApps:', error); res.status(500).json({ error: 'Internal server error' }); } }); // Refresh token for specific SubApp router.post('/:subAppId/token/refresh', authenticateUser, (req: AuthRequest, res) => { try { if (!req.user) { res.status(401).json({ error: 'Unauthorized' }); return; } const { subAppId } = req.params; const newToken = subAppService.refreshToken( req.user.userId, subAppId, req.user.role ); if (!newToken) { res.status(403).json({ error: 'Access denied to this SubApp' }); return; } res.json({ success: true, signedToken: newToken, expiresIn: 300, }); } catch (error) { console.error('Error refreshing token:', error); res.status(500).json({ error: 'Internal server error' }); } }); export default router; ``` ```typescript // src/app.ts import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import subAppRoutes from './routes/subapp.routes'; const app = express(); // Security middleware app.use(helmet()); app.use(cors({ origin: ['https://revoland.com', 'https://app.revoland.com'], credentials: true, })); app.use(express.json()); // Routes app.use('/api/v1/subapps', subAppRoutes); // Health check app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Metadata API server running on port ${PORT}`); }); export default app; ``` ### 2.3 SubApp (Flutter Web) Independent SubApp module that runs inside WebView. **Example: Property Listings SubApp** ```dart // property_listings/lib/main.dart import 'package:flutter/material.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'services/revoland_bridge.dart'; import 'services/api_service.dart'; import 'screens/listings_screen.dart'; void main() { setUrlStrategy(PathUrlStrategy()); runApp(const PropertyListingsApp()); } class PropertyListingsApp extends StatefulWidget { const PropertyListingsApp({Key? key}) : super(key: key); @override State createState() => _PropertyListingsAppState(); } class _PropertyListingsAppState extends State { late RevoLandBridge _bridge; late ApiService _apiService; bool _isReady = false; @override void initState() { super.initState(); _initializeSubApp(); } Future _initializeSubApp() async { _bridge = RevoLandBridge(); await _bridge.initialize(); final context = _bridge.getSuperAppContext(); if (context == null) { debugPrint('Error: SuperApp context not available'); return; } _apiService = ApiService( baseUrl: 'https://api.revoland.com', token: context.token, ); setState(() => _isReady = true); // Notify SuperApp that SubApp is ready _bridge.sendMessage({ 'action': 'subappReady', 'subAppId': context.subAppId, 'timestamp': DateTime.now().toIso8601String(), }); } @override Widget build(BuildContext context) { if (!_isReady) { return const MaterialApp( home: Scaffold( body: Center( child: CircularProgressIndicator(), ), ), ); } return MaterialApp( title: 'Property Listings', theme: ThemeData( primaryColor: Color( int.parse(_bridge.getSuperAppContext()?.theme['primaryColor'] ?.replaceFirst('#', '0xFF') ?? '0xFF2563eb'), ), brightness: _bridge.getSuperAppContext()?.theme['isDarkMode'] == true ? Brightness.dark : Brightness.light, ), home: ListingsScreen( apiService: _apiService, bridge: _bridge, ), ); } } ``` ```dart // property_listings/lib/services/revoland_bridge.dart import 'dart:convert'; import 'dart:html' as html; import 'package:flutter/foundation.dart'; class SuperAppContext { final String token; final String hostAppId; final String platform; final String userId; final String userRole; final String subAppId; final Map theme; final String locale; SuperAppContext({ required this.token, required this.hostAppId, required this.platform, required this.userId, required this.userRole, required this.subAppId, required this.theme, required this.locale, }); factory SuperAppContext.fromJson(Map json) { return SuperAppContext( token: json['token'] as String, hostAppId: json['hostAppId'] as String, platform: json['platform'] as String, userId: json['userId'] as String, userRole: json['userRole'] as String, subAppId: json['subAppId'] as String, theme: json['theme'] as Map, locale: json['locale'] as String, ); } } class RevoLandBridge { SuperAppContext? _context; final List)> _messageListeners = []; Future initialize() async { // Try to get context from window object _context = _getContextFromWindow(); if (_context == null) { // Try to get from URL params _context = _getContextFromUrl(); } // Set up callback for when context is injected _setupContextCallback(); debugPrint('RevoLand Bridge initialized with context: $_context'); } SuperAppContext? _getContextFromWindow() { try { final contextObj = html.window['__REVOLAND_CONTEXT__']; if (contextObj != null) { final Map contextMap = {}; final keys = js_util.getProperty(contextObj, 'keys'); // Convert JS object to Dart Map // This is a simplified version - actual implementation may vary return SuperAppContext.fromJson(contextMap); } } catch (e) { debugPrint('Error getting context from window: $e'); } return null; } SuperAppContext? _getContextFromUrl() { try { final uri = Uri.parse(html.window.location.href); final token = uri.queryParameters['token']; final hostAppId = uri.queryParameters['hostAppId']; if (token != null && hostAppId != null) { // Create minimal context from URL params return SuperAppContext( token: token, hostAppId: hostAppId, platform: 'web', userId: '', userRole: '', subAppId: '', theme: {'primaryColor': '#2563eb', 'isDarkMode': false}, locale: 'en', ); } } catch (e) { debugPrint('Error getting context from URL: $e'); } return null; } void _setupContextCallback() { html.window['onRevoLandContextReady'] = (dynamic context) { try { // Parse context and update debugPrint('Context received from SuperApp'); } catch (e) { debugPrint('Error in context callback: $e'); } }; } SuperAppContext? getSuperAppContext() => _context; void sendMessage(Map message) { try { final nativeBridge = html.window['RevoLandNativeBridge']; if (nativeBridge != null) { final messageStr = jsonEncode(message); // Call native bridge // nativeBridge.postMessage(messageStr); debugPrint('Sending message to SuperApp: $messageStr'); } else { debugPrint('Native bridge not available'); } } catch (e) { debugPrint('Error sending message: $e'); } } void requestLocation() { sendMessage({ 'action': 'requestLocation', 'timestamp': DateTime.now().toIso8601String(), }); } void shareProperty(String propertyId, String propertyUrl) { sendMessage({ 'action': 'shareProperty', 'propertyId': propertyId, 'propertyUrl': propertyUrl, 'timestamp': DateTime.now().toIso8601String(), }); } void openMap(double latitude, double longitude) { sendMessage({ 'action': 'openMap', 'latitude': latitude, 'longitude': longitude, 'timestamp': DateTime.now().toIso8601String(), }); } void navigateBack() { sendMessage({ 'action': 'navigateBack', 'timestamp': DateTime.now().toIso8601String(), }); } } ``` ```dart // property_listings/lib/services/api_service.dart import 'dart:convert'; import 'package:http/http.dart' as http; class Property { final String id; final String title; final String address; final double price; final int bedrooms; final int bathrooms; final double sqft; final String imageUrl; final double latitude; final double longitude; Property({ required this.id, required this.title, required this.address, required this.price, required this.bedrooms, required this.bathrooms, required this.sqft, required this.imageUrl, required this.latitude, required this.longitude, }); factory Property.fromJson(Map json) { return Property( id: json['id'] as String, title: json['title'] as String, address: json['address'] as String, price: (json['price'] as num).toDouble(), bedrooms: json['bedrooms'] as int, bathrooms: json['bathrooms'] as int, sqft: (json['sqft'] as num).toDouble(), imageUrl: json['imageUrl'] as String, latitude: (json['latitude'] as num).toDouble(), longitude: (json['longitude'] as num).toDouble(), ); } } class ApiService { final String baseUrl; final String token; ApiService({ required this.baseUrl, required this.token, }); Map get _headers => { 'Authorization': 'Bearer $token', 'Content-Type': 'application/json', }; Future> fetchProperties({ String? city, double? minPrice, double? maxPrice, int? minBedrooms, }) async { final queryParams = {}; if (city != null) queryParams['city'] = city; if (minPrice != null) queryParams['minPrice'] = minPrice.toString(); if (maxPrice != null) queryParams['maxPrice'] = maxPrice.toString(); if (minBedrooms != null) queryParams['minBedrooms'] = minBedrooms.toString(); final uri = Uri.parse('$baseUrl/api/v1/properties').replace( queryParameters: queryParams.isEmpty ? null : queryParams, ); final response = await http.get(uri, headers: _headers); if (response.statusCode == 200) { final data = jsonDecode(response.body); final List propertiesJson = data['properties']; return propertiesJson.map((json) => Property.fromJson(json)).toList(); } else if (response.statusCode == 401) { throw Exception('Unauthorized - token expired'); } else { throw Exception('Failed to load properties'); } } Future getPropertyDetails(String propertyId) async { final uri = Uri.parse('$baseUrl/api/v1/properties/$propertyId'); final response = await http.get(uri, headers: _headers); if (response.statusCode == 200) { final data = jsonDecode(response.body); return Property.fromJson(data['property']); } else if (response.statusCode == 401) { throw Exception('Unauthorized - token expired'); } else { throw Exception('Failed to load property details'); } } } ``` ```dart // property_listings/lib/screens/listings_screen.dart import 'package:flutter/material.dart'; import '../services/api_service.dart'; import '../services/revoland_bridge.dart'; class ListingsScreen extends StatefulWidget { final ApiService apiService; final RevoLandBridge bridge; const ListingsScreen({ Key? key, required this.apiService, required this.bridge, }) : super(key: key); @override State createState() => _ListingsScreenState(); } class _ListingsScreenState extends State { List _properties = []; bool _isLoading = true; String? _error; @override void initState() { super.initState(); _loadProperties(); } Future _loadProperties() async { setState(() { _isLoading = true; _error = null; }); try { final properties = await widget.apiService.fetchProperties(); setState(() { _properties = properties; _isLoading = false; }); } catch (e) { setState(() { _error = e.toString(); _isLoading = false; }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Property Listings'), leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => widget.bridge.navigateBack(), ), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _error != null ? Center(child: Text('Error: $_error')) : ListView.builder( padding: const EdgeInsets.all(16), itemCount: _properties.length, itemBuilder: (context, index) { final property = _properties[index]; return _PropertyCard( property: property, onTap: () => _showPropertyDetails(property), onShare: () => widget.bridge.shareProperty( property.id, 'https://revoland.com/properties/${property.id}', ), onShowMap: () => widget.bridge.openMap( property.latitude, property.longitude, ), ); }, ), ); } void _showPropertyDetails(Property property) { // Navigate to property details screen } } class _PropertyCard extends StatelessWidget { final Property property; final VoidCallback onTap; final VoidCallback onShare; final VoidCallback onShowMap; const _PropertyCard({ required this.property, required this.onTap, required this.onShare, required this.onShowMap, }); @override Widget build(BuildContext context) { return Card( margin: const EdgeInsets.only(bottom: 16), child: InkWell( onTap: onTap, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Image.network( property.imageUrl, height: 200, width: double.infinity, fit: BoxFit.cover, ), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( property.title, style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( property.address, style: Theme.of(context).textTheme.bodyMedium, ), const SizedBox(height: 8), Text( '\$${property.price.toStringAsFixed(0)}', style: Theme.of(context).textTheme.headlineSmall?.copyWith( color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Row( children: [ _PropertyFeature( icon: Icons.bed, label: '${property.bedrooms} beds', ), const SizedBox(width: 16), _PropertyFeature( icon: Icons.bathroom, label: '${property.bathrooms} baths', ), const SizedBox(width: 16), _PropertyFeature( icon: Icons.square_foot, label: '${property.sqft.toStringAsFixed(0)} sqft', ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( icon: const Icon(Icons.map), onPressed: onShowMap, ), IconButton( icon: const Icon(Icons.share), onPressed: onShare, ), ], ), ], ), ), ], ), ), ); } } class _PropertyFeature extends StatelessWidget { final IconData icon; final String label; const _PropertyFeature({ required this.icon, required this.label, }); @override Widget build(BuildContext context) { return Row( children: [ Icon(icon, size: 16), const SizedBox(width: 4), Text(label), ], ); } } ``` ### 2.4 SubApp Backend API Backend service for SubApp data. ```typescript // src/middleware/subapp-auth.middleware.ts import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { JWTPayload } from '../types/subapp.types'; const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; export interface SubAppAuthRequest extends Request { subAppAuth?: JWTPayload; } export const authenticateSubApp = ( req: SubAppAuthRequest, res: Response, next: NextFunction ): void => { try { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { res.status(401).json({ error: 'No token provided' }); return; } const token = authHeader.substring(7); const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload; // Verify token hasn't expired const now = Math.floor(Date.now() / 1000); if (decoded.exp < now) { res.status(401).json({ error: 'Token expired' }); return; } // Verify hostAppId if (decoded.hostAppId !== 'revoland-mobile') { res.status(403).json({ error: 'Invalid host app' }); return; } req.subAppAuth = decoded; next(); } catch (error) { console.error('Token verification error:', error); res.status(401).json({ error: 'Invalid token' }); } }; export const requirePermission = (permission: string) => { return (req: SubAppAuthRequest, res: Response, next: NextFunction): void => { if (!req.subAppAuth) { res.status(401).json({ error: 'Unauthorized' }); return; } const { permissions } = req.subAppAuth; if (!permissions.includes(permission) && !permissions.includes('*')) { res.status(403).json({ error: `Missing required permission: ${permission}` }); return; } next(); }; }; ``` ```typescript // src/routes/properties.routes.ts import { Router } from 'express'; import { SubAppAuthRequest, authenticateSubApp, requirePermission } from '../middleware/subapp-auth.middleware'; const router = Router(); // Mock property data const properties = [ { id: 'prop-001', title: 'Modern Downtown Condo', address: '123 Main St, Downtown', price: 450000, bedrooms: 2, bathrooms: 2, sqft: 1200, imageUrl: 'https://cdn.revoland.com/properties/prop-001.jpg', latitude: 40.7128, longitude: -74.0060, description: 'Beautiful modern condo in the heart of downtown', }, { id: 'prop-002', title: 'Spacious Family Home', address: '456 Oak Ave, Suburbia', price: 650000, bedrooms: 4, bathrooms: 3, sqft: 2500, imageUrl: 'https://cdn.revoland.com/properties/prop-002.jpg', latitude: 40.7589, longitude: -73.9851, description: 'Perfect family home with large backyard', }, ]; // Get all properties router.get( '/', authenticateSubApp, requirePermission('read:properties'), (req: SubAppAuthRequest, res) => { try { const { city, minPrice, maxPrice, minBedrooms } = req.query; let filtered = [...properties]; if (city) { filtered = filtered.filter((p) => p.address.toLowerCase().includes((city as string).toLowerCase()) ); } if (minPrice) { filtered = filtered.filter((p) => p.price >= Number(minPrice)); } if (maxPrice) { filtered = filtered.filter((p) => p.price <= Number(maxPrice)); } if (minBedrooms) { filtered = filtered.filter((p) => p.bedrooms >= Number(minBedrooms)); } res.json({ success: true, properties: filtered, count: filtered.length, userId: req.subAppAuth?.userId, }); } catch (error) { console.error('Error fetching properties:', error); res.status(500).json({ error: 'Internal server error' }); } } ); // Get property by ID router.get( '/:propertyId', authenticateSubApp, requirePermission('read:properties'), (req: SubAppAuthRequest, res) => { try { const { propertyId } = req.params; const property = properties.find((p) => p.id === propertyId); if (!property) { res.status(404).json({ error: 'Property not found' }); return; } res.json({ success: true, property, }); } catch (error) { console.error('Error fetching property:', error); res.status(500).json({ error: 'Internal server error' }); } } ); export default router; ``` ## 3. Workflow 1. User logs into RevoLand SuperApp 2. SuperApp calls Metadata API to get available SubApps and launch tokens 3. User selects a SubApp (e.g., "Property Listings") 4. SuperApp initializes WebView with SubApp URL and injects context with short-lived JWT 5. SubApp reads token and calls backend API to fetch property data 6. Backend validates JWT and returns data if valid 7. SubApp and SuperApp exchange messages via JS Bridge for native features ## 4. Security Mechanisms ### 4.1 Short-lived JWT Tokens - Each SubApp launch generates a new JWT with 5-minute expiration - Token contains: userId, hostAppId, subAppId, userRole, permissions, exp - Tokens can be refreshed through Metadata API ### 4.2 Backend Validation - All SubApp API calls require valid JWT - Backend verifies signature, expiration, and hostAppId - Rejects requests with expired or invalid tokens ### 4.3 Access Control - CORS restricted to CDN domains only - User-agent validation to confirm WebView origin - Permission-based access to SubApps and features ### 4.4 Secure Communication - HTTPS enforced for all communications - No sensitive data embedded in SubApp source code - Minimal data exposure through JS Bridge ## 5. Benefits - **Fast Updates**: Deploy SubApp updates without republishing SuperApp - **Independent Development**: Teams work on SubApps in isolation - **Security**: Dynamic tokens and server-side validation - **Flexibility**: Add/remove SubApps via metadata without code changes - **Scalability**: CDN hosting for SubApps ensures global performance ## 6. Architecture Diagram ``` [User] ↓ Login [RevoLand SuperApp - Flutter Native] ↓ HTTPS + Bearer Token [Metadata API] → Returns SubApps + JWT tokens ↓ Display SubApp Menu (Property Listings, Virtual Tours, etc.) ↓ User selects SubApp [WebView] ↔ JS Bridge ↔ [SubApp - Flutter Web] ↓ API calls with Bearer JWT [SubApp Backend API] ↓ Validate JWT → Return data [User sees property listings, tours, calculators, etc.] ``` ## 7. Example API Response **Metadata API Response:** ```json { "success": true, "subApps": [ { "id": "property-listings", "displayName": "Property Listings", "description": "Browse available properties", "iconUrl": "https://cdn.revoland.com/icons/listings.png", "launchUrl": "https://cdn.revoland.com/subapps/property-listings/index.html", "signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "permissions": ["read:properties", "search:properties"], "version": "1.2.0", "minHostVersion": "1.0.0" }, { "id": "virtual-tours", "displayName": "Virtual Tours", "description": "360° property tours", "iconUrl": "https://cdn.revoland.com/icons/virtual-tour.png", "launchUrl": "https://cdn.revoland.com/subapps/virtual-tours/index.html", "signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "permissions": ["read:properties", "view:tours"], "version": "2.0.1", "minHostVersion": "1.0.0" }, { "id": "mortgage-calculator", "displayName": "Mortgage Calculator", "description": "Calculate mortgage payments", "iconUrl": "https://cdn.revoland.com/icons/calculator.png", "launchUrl": "https://cdn.revoland.com/subapps/mortgage-calculator/index.html", "signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "permissions": ["calculate:mortgage"], "version": "1.5.2", "minHostVersion": "1.0.0" }, { "id": "agent-dashboard", "displayName": "Agent Dashboard", "description": "Manage your properties and leads", "iconUrl": "https://cdn.revoland.com/icons/dashboard.png", "launchUrl": "https://cdn.revoland.com/subapps/agent-dashboard/index.html", "signedToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "permissions": ["manage:properties", "manage:leads", "view:analytics"], "version": "3.1.0", "minHostVersion": "1.0.0" } ], "timestamp": "2025-10-23T10:30:00Z" } ``` **SubApp API Response (Properties):** ```json { "success": true, "properties": [ { "id": "prop-001", "title": "Modern Downtown Condo", "address": "123 Main St, Downtown", "price": 450000, "bedrooms": 2, "bathrooms": 2, "sqft": 1200, "imageUrl": "https://cdn.revoland.com/properties/prop-001.jpg", "latitude": 40.7128, "longitude": -74.0060, "description": "Beautiful modern condo in the heart of downtown" }, { "id": "prop-002", "title": "Spacious Family Home", "address": "456 Oak Ave, Suburbia", "price": 650000, "bedrooms": 4, "bathrooms": 3, "sqft": 2500, "imageUrl": "https://cdn.revoland.com/properties/prop-002.jpg", "latitude": 40.7589, "longitude": -73.9851, "description": "Perfect family home with large backyard" } ], "count": 2, "userId": "user-123" } ``` --- **Document Version:** 1.0 **Last Updated:** October 23, 2025 **Project:** RevoLand - Real Estate SuperApp Platform