dynamic_backend_bridge
A Flutter package that provides a dynamic, decoupled interface for switching between Supabase backends (Managed Cloud, Custom Supabase Cloud, and Self-Hosted Docker instances) at runtime. It includes a built-in dark/light themed onboarding wizard UI, a unified Auth layer, a generic map-based Database layer with batch and query filtering support, Riverpod state providers, local & push notifications, and customizable in-app banner alerts.
Features
- Runtime Backend Switching: Swap between a Managed Supabase Cloud Backend, a Custom
supabase.comproject, or a self-hosted Docker VPS instance dynamically without rebuilds. - Riverpod State Management: Access authentication state, database repository, theme settings, and notification services via first-class Riverpod providers (
core_providers.dart). - Unified Authentication: Perform sign-in, sign-up, sign-out, session restoration, and connection health checks via a single abstract interface (
AuthRepository). - Generic Database Bridge: Read, write, batch upsert/delete, and stream records reactively using a generic, map-based repository interface (
DatabaseRepository). - Type-Safe Collections (
TypedCollection<T>): Wrap the database repository with models to serialize, deserialize, batch process, filter, order, and paginate data. - Push & Local Notifications: Built-in timezone-aware local notification scheduling (
NotificationService) and Firebase Cloud Messaging integration (RemoteNotificationService). - In-App Banner Notifications: Floating overlay banner alerts (
AppBannerService) with auto-dismiss timers, action callbacks, and seamless foreground FCM message display. - Theme & Appearance Service: Persistent theme mode management (
ThemeService,themeModeProvider) and configurable design themes (AppTheme). - Ready-to-use UI Components: Clean, responsive UI components including
HostingWizard,SignInPage,DynamicProfilePage, andAppBannerWidget.
Backend Modes
dynamic_backend_bridge supports two runtime deployment modes:
BackendType.managed(Our Managed Cloud): Connects to your organization's default managed Supabase instance. The app developer suppliesdefaultSupabaseUrlanddefaultSupabaseAnonKeyprogrammatically (or viaAppEnvironment/--dart-define). End users do not need to configure anything.BackendType.customSupabase(Your Own Supabase Server): Allows end-users or administrators to enter their own Supabase Project URL & Anon Key viaHostingWizard. Works seamlessly for bothsupabase.comCloud projects and private self-hosted Docker VPS servers.
Getting Started
Add the package to your pubspec.yaml dependencies:
dependencies:
dynamic_backend_bridge: ^0.0.7
Run flutter pub get to install the dependencies.
Usage
1. Initialize the Bridge
In your application's main() or bootstrapping sequence, check for any saved configuration and initialize the dynamic backend bridge:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:dynamic_backend_bridge/dynamic_backend_bridge.dart';
// Centralize credentials in a single environment class or --dart-define
class AppEnvironment {
static const String defaultSupabaseUrl = String.fromEnvironment(
'SUPABASE_URL',
defaultValue: 'https://xyzcompany.supabase.co',
);
static const String defaultSupabaseAnonKey = String.fromEnvironment(
'SUPABASE_ANON_KEY',
defaultValue: 'eyJhbGciOiJIUzI1NiIsInR...',
);
}
// Helper function to initialize backend
Future<void> initializeBackend(AppConfig config, ProviderContainer container) async {
await DynamicBackendBridge.initialize(
config: config,
container: container,
defaultSupabaseUrl: AppEnvironment.defaultSupabaseUrl,
defaultSupabaseAnonKey: AppEnvironment.defaultSupabaseAnonKey,
);
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferences.getInstance();
final configService = ConfigService();
final savedConfig = await configService.getSavedConfig();
final container = ProviderContainer(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
],
);
if (savedConfig != null) {
await initializeBackend(savedConfig, container);
}
runApp(UncontrolledProviderScope(
container: container,
child: MyApp(
configService: configService,
initialConfig: savedConfig,
),
));
}
2. Riverpod State Providers
Access core backend services anywhere in your widget tree using Riverpod:
// Access database repository
final db = ref.watch(databaseRepositoryProvider);
// Access auth repository
final auth = ref.watch(authRepositoryProvider);
// Listen to auth state stream
final userAsync = ref.watch(currentUserProvider);
// Manage theme mode (System / Light / Dark)
final themeMode = ref.watch(themeModeProvider);
ref.read(themeModeProvider.notifier).setThemeMode(ThemeMode.dark);
// Access notification services
final localNotif = ref.watch(notificationServiceProvider);
final remoteNotif = ref.watch(remoteNotificationServiceProvider);
3. Database Layer & Typed Collections
Wrap domain models using TypedCollection<T> for type-safe database queries, batch operations, and realtime streaming:
class Task {
final String id;
final String title;
final String userId;
final bool isCompleted;
Task({
required this.id,
required this.title,
required this.userId,
this.isCompleted = false,
});
Map<String, dynamic> toMap() => {
'title': title,
'userId': userId,
'is_completed': isCompleted,
};
static Task fromMap(Map<String, dynamic> map, String id) => Task(
id: id,
title: map['title'] ?? '',
userId: map['userId'] ?? '',
isCompleted: map['is_completed'] ?? false,
);
}
// Instantiate collection wrapper
final taskCollection = TypedCollection<Task>(
repo: ref.read(databaseRepositoryProvider),
collectionName: 'tasks',
toMap: (task) => task.toMap(),
fromMap: (map, id) => Task.fromMap(map, id),
);
// Save single item
await taskCollection.save(Task(id: '', title: 'Buy milk', userId: 'user123'), 'task-id-1');
// Batch save multiple items
await taskCollection.saveBatch([
Task(id: '1', title: 'Task 1', userId: 'user123'),
Task(id: '2', title: 'Task 2', userId: 'user123'),
]);
// Query with filtering, ordering, and pagination
final openTasks = await taskCollection.fetch(
filters: [
QueryFilter.eq('userId', 'user123'),
QueryFilter.eq('is_completed', false),
],
orderBy: 'title',
ascending: true,
limit: 20,
);
// Watch realtime updates
final taskStream = taskCollection.watch(
filters: [QueryFilter.eq('userId', 'user123')],
);
// Batch delete
await taskCollection.deleteBatch(['1', '2']);
4. In-App Banner Notifications
Display non-blocking in-app alert banners anywhere in your application:
// Success Banner
AppBannerService.showSuccess(context, 'Profile updated successfully!');
// Error Banner
AppBannerService.showError(context, 'Failed to save changes. Please try again.');
// Custom Informational Banner with Tap Handler
AppBannerService.showInfo(
context,
title: 'New Update Available',
body: 'Tap to view release details.',
duration: const Duration(seconds: 5),
onTap: () {
// Navigate to release page
},
);
// Programmatically dismiss current banner
AppBannerService.hideCurrentBanner();
5. Local & Push Notifications
Schedule local notifications or configure Firebase Cloud Messaging:
final notifService = ref.read(notificationServiceProvider);
// Schedule a notification in 10 minutes
await notifService.scheduleNotification(
id: 101,
title: 'Task Reminder',
body: 'Time to complete your scheduled task!',
duration: const Duration(minutes: 10),
);
// Show immediate notification
await notifService.showNotification(
id: 102,
title: 'Welcome!',
body: 'Thank you for joining our platform.',
);
// Cancel notification
await notifService.cancelNotification(101);
6. Built-in UI Components
Hosting Wizard (Onboarding & Endpoint Configuration)
HostingWizard(
configService: configService,
onValidate: (AppConfig config) async {
try {
final tempContainer = ProviderContainer();
await initializeBackend(config, tempContainer);
final auth = tempContainer.read(authRepositoryProvider);
return await auth.validateConnection();
} catch (e) {
return e.toString();
}
},
onComplete: (AppConfig config) async {
await DynamicBackendBridge.initialize(
config: config,
ref: ref,
defaultSupabaseUrl: AppEnvironment.defaultSupabaseUrl,
defaultSupabaseAnonKey: AppEnvironment.defaultSupabaseAnonKey,
);
},
)
Dynamic Profile & Appearance Settings Page
DynamicProfilePage(
configService: configService,
onBackendConfigPressed: () {
// Navigate to HostingWizard or config modal
},
)
Additional Information
For issues, contributions, or configuration details, please refer to the GitHub repository.