web_ui_plugin
π WebUI Plugins: The SaaS Builder's Dream
Building SaaS applications doesn't have to be complex. WebUI Plugins is a modular, plug-and-play framework for Flutter Web that brings professional design and functionality without the architectural headache.
What makes it different?
- β¨ Minimal Setup β Define your data models and UI, let the framework handle the rest.
- π Automatic Magic β Automatically generates forms, validation, and data handling logic.
- β‘ Production-Ready β Built with Firebase integration out of the box.
- π¨ Web-Grade UI β Professional, responsive design that feels native to the browser.
- π¦ Truly Modular β Register plugins, build features independently, and scale effortlessly.
Perfect for: Bootstrapped founders, indie hackers, and dev teams who want to ship SaaS faster without sacrificing quality.
The 4-Step Developer Experience
1. Define your Data Model
class PetOwnerModel extends DataModel {
final String? id, name, mobile;
PetOwnerModel({this.id, this.name, this.mobile});
@override Map<String, dynamic> toJson() => {'id': id, 'name': name, 'mobile': mobile};
factory PetOwnerModel.fromJson(Map<String, dynamic> json) => ...;
@override String? get uid => id;
}
2. Create the Declarative UI
Use FormPageView with WidgetConfig. The framework handles the layout and state automatically.
initialTabDetailBuilder: (item, ctx) => FormPageView(
fields: [
WidgetConfig(key: 'name', fieldType: FieldType.name, labelText: 'Full Name'),
WidgetConfig(key: 'mobile', fieldType: FieldType.mobileNumber, labelText: 'Mobile'),
],
rebuildDataModel: (data) => PetOwnerModel.fromJson(data),
)
3. Register the Plugin Descriptor
Define identity, permissions, and routing in a single object.
final petOwnerPlugin = PluginDescriptor<PetOwnerModel>(
moduleId: 'pet-owners',
title: 'Pet Owners',
icon: Icons.person,
dataBinding: PluginDataBinding<PetOwnerModel>(
collectionName: 'petOwners',
fromJson: PetOwnerModel.fromJson,
createEmpty: PetOwnerModel.new,
),
routes: [ ... ],
);
4. Bootstrap and Run
Initialize the framework and register your plugins in main.dart.
void main() async {
await AppBootstrap.initialize(config: BootstrapConfig(...));
await AppBootstrap.registerPlugins([petOwnerPlugin]);
runApp(AppBootstrap.buildRouterApp(
title: 'My SaaS App',
shellBuilder: (context, child) => MyShell(child: child),
));
}
ποΈ Feature Status (Current State)
- β Modular Registry: Plugin system is fully operational.
- β Firebase Integration: Firestore CRUD and Realtime streams are live.
- β Permission System: Persona-based sidebar and route gating is live.
- β Scoped Repositories: Individual data isolation per plugin (Backlog #4 Fixed).
- π§ Image Uploads
WORK IN PROGRESS:UploadCapabilitycontract is defined; Firebase Storage adapter implementation is underway. - π§ Theme Engine & Dark Mode
WORK IN PROGRESS: Base theming is available; automatic switching and deep customization are being refined.
Core Framework Architecture
- PluginRegistry: Central source of truth for all modules.
- ScopedRepo: Isolated data access layer per module (Backend-agnostic).
- SectionWidget: High-performance two-pane master/detail layout.
- PermissionMiddleware: Dual-layer security (Sidebar visibility + Route guards).
Roadmap π£οΈ
12. Known Issues & Improvement Backlog
Issues found during architecture review (April 2026). Ordered by severity.
#1 β DataModel.uid typed String? but semantically required β MEDIUM
File: lib/src/core/contracts/data_model.dart
Problem: String? get uid; // not null β the comment contradicts the type. Every downstream lookup (item.uid == id) must null-check unnecessarily.
Fix: Change to String get uid. All concrete models must provide a non-null uid, surfacing missing IDs at compile time.
#2 β UploadCapability stored in BootstrapConfig but never injected into plugins β MEDIUM
File: lib/src/core/bootstrap/app_bootstrap.dart
Problem: BootstrapConfig.uploadCapability is accepted but never passed to FormCubit or PluginDescriptor. Plugins that declare supportsUpload: true have no access to the capability at runtime.
Fix: Pass uploadCapability through AppBootstrap._buildCubits or expose it via a RepositoryProvider<UploadCapability>.
#4 β FormRepoMixin.update and FormCubit.updateItem expose an index β MEDIUM
Files: form_repo_mixin.dart, form_cubit.dart
Problem: update(int index, T item) β callers must track a list position. The underlying service finds items by id, not index; the index only updates the local cache.
Fix: Change signature to update(T item) and find the cache index internally via items.indexWhere((e) => e.uid == item.uid).
#5 β ScopedRepo uses (service as dynamic).collectionName β MEDIUM
File: lib/src/adapters/firebase/scoped_repo.dart
Problem: Dynamic cast to read collectionName silently falls back to T.toString() if the cast fails, producing a wrong registry key.
Fix: Define abstract interface CollectionNamed { String get collectionName; }, implement it on FirestoreService, and cast to CollectionNamed instead of dynamic.
#6 β No onError handler on realtime stream subscriptions β MEDIUM
Files: section_cubit.dart, form_repo_mixin.dart
Problem: _repoStream.listen((data) { ... }) has no onError callback. A Firestore permission error or network failure silently cancels the subscription with no state update.
Fix: Add onError: (error) => emit(state.copyWith(status: SuccessStatus.failure)) (and equivalent in FormRepoMixin).
#7 β SectionState.addedItemId skips the sentinel pattern in copyWith β LOW
File: lib/src/core/section/cubit/section_state.dart
Problem: Every call to copyWith(searchText: 'x') silently resets addedItemId to null because it does not fall back to this.addedItemId.
Fix: Apply the same static const Object _unset sentinel pattern used by selectedItem and fromDate.
#8 β Globals.hasUnsavedFormChanges is a mutable global static β MEDIUM
File: lib/src/core/contracts/globals.dart
Problem: FormPageView writes this flag and PluginLeftNavigation reads it, but nothing reacts to changes β no stream, no notifier. The flag can also become stale between page navigations.
Fix: Move hasUnsavedChanges into FormCubit's state (or a ValueNotifier). Navigation widgets subscribe to it reactively instead of polling a global.
Application Images
|
|
|
|
-- firebase emulators:start --import=./emulator-data --export-on-exit
Libraries
- web_ui_plugin
- web_ui_plugin β Plug-and-play SaaS admin UI package.