How I Built a Zero-Trust Boundary Engine for Flutter Clean Architecture: Introducing Zuq CLI ⚡

I am Software Engineer(Mobile) who is particularly enthusiastic about leveraging modern technologies to create user-friendly and scalable applications. I thrive in collaborative environments and enjoy working with diverse teams to deliver exceptional software solutions. Follow along as I share my knowledge, insights, and experiences on Hashnode. I look forward to connecting with fellow developers, learning from the community, and contributing to the growth of the tech industry.
Picture this: It's 11:45 PM. The production deployment is in fifteen minutes, and a critical auth bug just reared its head. In a caffeinated rush, a developer bypasses the domain repository interfaces, imports auth_api_repository_impl.dart directly into a UI widget, patches the bug, and pushes.
The build passes, tests run successfully, and the hotfix is live. Disaster averted, right?
Fast forward six months. Your simple Flutter app has grown into a large product. But compile times have tripled, widgets are tightly coupled to database drivers, and refactoring a single REST endpoint requires changes in seven different folders. What started as a pristine "Clean Architecture" codebase has slowly rotted into a spaghetti-infused monolith.
This is the erosion problem. Clean Architecture is relatively easy to set up on Day 1, but it is notoriously difficult to maintain as a team grows.
To solve this, I built Zuq CLI, a flexible, boundary-enforcing scaffold and audit tool for Flutter Clean Architecture.
In this post, we’ll dive into why I built it, how it works under the hood (including a topological dependency resolver and static import analysis), and how you can use it to build bulletproof Flutter apps.
The Problem: The High Cost of Broken Boundaries
Clean Architecture separates code into three distinct layers:
Domain: The core business logic (Entities, Use Cases, Repositories interfaces). It must be pure Dart and completely agnostic of UI and databases.
Data: The data sources, API clients, and repository implementations.
Presentation: The UI widgets and state management (BLoC, Riverpod, etc.).
These layers must adhere to a strict Dependency Rule: dependencies can only point inward.
Presentation → Domain ← Data
└─────── ✕ ──┘
(No direct imports!)
In the real world, developers make mistakes. A misplaced import, such as import 'package:app/features/auth/data/models/user_model.dart' in a presentation widget, silently breaks the boundary. Because Dart doesn't prevent this compile-time import, these violations accumulate unnoticed until the codebase becomes a tangled web of circular dependencies.
I wanted a tool that would:
Scaffold clean slices automatically.
Prevent me from adding modules out-of-order.
Statically audit imports in CI to fail the build if a boundary is violated.
Welp, here comes Zuq and its prowess:
1. Scaffolding in Seconds: zuq create
Bootstrapping a Flutter project with clean architecture is usually a chore of folder creation (my lazy self found this cumbersome and boring) and boilerplate copying.
With Zuq, you can bootstrap a production-ready template in a single command, configured for your favorite state management and routing solution (Currently supports go_router and auto_route).
dart pub global activate zuq_cli
zuq create my_fintech_app --state bloc --router go_router
If you don't supply the flags, Zuq prompts you interactively.
2. Once you scaffold your first feature with zuq add feature auth, the generated folder structure isolates concerns cleanly:
lib/features/auth/
data/
models/
repositories/
domain/
entities/
repositories/
usecases/
presentation/
pages/
state/ <- Pre-populated with selected state boilerplate
widgets/
3. Smart Module Installation with Topological Resolution
Adding a core module, like networking or storage, often requires adding a package to pubspec.yaml, writing config singletons and ensuring dependencies don't loop.
Zuq handles this with zuq add module <module_name>. But the secret sauce is its Topological Dependency Resolver.
Suppose you want to install the networking module. In Zuq, the dependency graph looks like this:
networkingdepends onstorage(for auth tokens) andanalytics(for logging telemetry).storageandanalyticshave no dependencies.
When you run:
zuq add module networking
Zuq doesn't just download package dependencies. It parses the dependency graph, runs a Depth-First Search (DFS) topological sort with cycle detection, and resolves the correct install order:
Resolved installation order: analytics → storage → networking
Under the Hood: The Resolver
The CLI builds an internal graph of modules and uses a recursion stack check to find circular dependencies before starting any write operations:
void dfs(String node) {
if (recursionStack.contains(node)) {
throw CircularDependencyException('Circular dependency detected: $node');
}
if (visited.contains(node)) return;
recursionStack.add(node);
final deps = _graph[node] ?? [];
for (final dep in deps) {
dfs(dep);
}
recursionStack.remove(node);
visited.add(node);
resolved.add(node); // Post-order collection
}
Once resolved, Zuq executes code injection templates (using mason under the hood) and installs the required pub dependencies sequentially.
4. The Boundary Guard: zuq doctor
This is the core differentiator of Zuq. It is a static analyzer that audits folder boundaries. It answers the question: Are my layer boundaries being respected?
When you run:
zuq doctor
Zuq scans every Dart file in your lib/features directory, ignores comments, parses all import statements, and canonicalizes relative paths to project-level imports. It then validates imports against these three absolute rules:
| Source Layer | Target Layer | Allowed? | Rationale |
|---|---|---|---|
| Domain | Presentation | ❌ Banned | Core business logic should never know about UI layouts. |
| Domain | Data | ❌ Banned | Domain must be database/network agnostic. |
| Presentation | Data | ❌ Banned | Widgets must depend on Usecase interfaces, not API clients or models. |
| Data | Presentation | ❌ Banned | Data repository implementations have no business with visual controls. |
How It Looks When Someone Violates a Boundary
If a developer tries to sneak a data import into a presentation widget:
$ zuq doctor
Auditing project architectural boundaries for my_app...
Violation in lib/features/auth/presentation/pages/login_page.dart:12
Layer: presentation → Target Layer: data
Import: package:my_app/features/auth/data/datasources/auth_remote_source.dart
Reason: The Presentation layer should not depend directly on the Data layer (use domain/repositories instead).
✗ Architecture audit failed: 1 violation found
By exiting with code 1, it is custom-built to integrate directly into Git pre-commit hooks or your continuous integration pipeline.
5. Zero-Trust Architecture in CI
To enforce this boundary verification automatically across your team, you can add zuq doctor to your GitHub Actions. It runs in milliseconds because it does static analysis on text imports rather than spinning up heavy compiler services:
name: Architecture Audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
channel: stable
- name: Install Zuq CLI
run: dart pub global activate zuq_cli
- name: Run boundary audit
run: zuq doctor
If any developer tries to push a PR with direct boundary violations, the workflow fails, preventing bad practices from ever merging into main.
How to Get Started
Zuq is open-source and active on Pub.dev.
To give it a spin:
Install Zuq:
dart pub global activate zuq_cliCreate your project:
zuq create my_awesome_app --state riverpod --router auto_routeGenerate your first feature slice:
zuq add feature user_profileAudit and enjoy a clean codebase:
zuq doctor
I built Zuq to turn Clean Architecture from a guide in a book into something your CI pipeline can actually enforce. Try it on your next Flutter project and let me know what you think!
Have features you'd like to see, or custom templates? Check out the source code at GitHub - Morizuq/zuq_cli. Contributions, stars, and feedback are always welcome! 🚀

