Chassis ποΈ
An opinionated architecture framework for Flutter.

Chassis provides a rigid structure for your apps so you can stop worrying about architecture and focus on writing code that matters. Because this structure is enforced by the framework rather than by convention, it prevents architectural drift as your codebase grows, without relying on developer discipline.
Chassis is built for the AI era. Its strictness naturally acts as rails for coding agents, and it ships with ready-to-use AI skills.
Overview
Chassis combines MVVM, command-query separation, and the mediator pattern. It consists of three packages:
chassis β pure Dart primitives: commands, queries, handlers, and the mediator
chassis_flutter β a powerful ViewModel class and reactive widgets to connect your business logic to the widget tree
chassis_builder β generates the app mediator and verifies at build time that every message has a handler
New here? Start with the Quick Start β a complete todo app in 15 minutes. Then dive into the guides:
Core Architecture β the layers, command-query separation, and the mediator pattern
Business Logic β messages and handlers: validation, orchestration, and testing in isolation
Code Generation β
@chassisHandler,@ChassisApp, modules, and the generator's build-time guaranteesUI Integration β ViewModels, dispatching messages, rendering
Async<T>, and one-time eventsHard Cases β optimistic UI, deduplicating watch subscriptions, paginated lists, and what
restartabledoes not doWhat a Feature Costs β an honest artifact-count comparison with Bloc and Riverpod, and what the ceremony buys
Error Management β the error strategy end-to-end, from infrastructure to the UI
Coding Rules β the framework's implementation rules in DO/DON'T form
How is this different from Bloc or Riverpod?
Ask any Flutter stack one question: where does business logic live?
Bloc is opinionated about presentation β events in, states out β and its official answer is that business logic lives inside blocs. In practice, one class holds both your use cases and your UI state machine: logic gets tangled with presentation, reusing it across blocs is awkward, and some of it inevitably drifts into repositories.
Riverpod gives you a reactive dependency graph, deliberately unopinionated about layers: any provider can depend on any other, and layering is entirely up to your team's discipline.
Chassis implements Flutter's recommended app architecture β MVVM, commands, repositories. Business logic is a layer of its own. You describe each operation as a typed message β CreateUserCommand, WatchCartQuery β and write its logic in a handler, a plain class that receives the message and works with your repositories. ViewModels never touch a repository: they dispatch messages and hold UI state, nothing else. Every feature has the same three parts β a message, a handler, a ViewModel β so any piece of code has one obvious place to live, and the build fails when the structure is broken: a message without a handler is a build error, and so is a handler that tries to reach back into the mediator.
See it in action
Pulse β coming soon
A real-time activity feed demonstrating WatchQuery streams and RunPolicy concurrency.
Ledger β coming soon
An expense tracker demonstrating @chassisModule and the multi-package split.
Chassis at a glance
Chassis embraces architectural purity. It does require more boilerplate than most state management packages. But you aren't supposed to write this boilerplate by hand. Modern LLMs will generate it effortlessly.
Business logic is written as command and query handlers. Commands and queries are pure Dart objects dispatched by the UI: they carry the parameters the handler needs to apply your business logic.
@chassisHandler registers the handler in a generated mediator: a single interception point for middlewares like logging and crash reporting. The generator also verifies the wiring β a command or query with no handler fails the build at its declaration site, so an unhandled message can never reach runtime.
On the UI side, a ViewModel holds an immutable state, dispatches command and query objects through the mediator installed once in main() with Chassis.initialize(AppMediator(...)), and offers an event channel for one-time notifications like snackbars and navigation. ViewModels depend only on the message types, never on the generated mediator class. Asynchronous state fields are typed Async<T>, a sealed type covering loading, data, and error:
run reports the whole lifecycle as Async<T> states, so there are no hand-written isLoading flags or try/catch blocks β and no forgotten error path: onState fires for every transition (loading, data, error), while onSuccess and onError(error, stack) are additive conveniences on top of it. read and watch offer the same contract for one-shot and reactive queries, and a RunPolicy decides how concurrent dispatches interact (debounced, restartable, etc).
In the widget tree, context.select subscribes the widget to exactly the field it renders, and since Async<T> is sealed, a switch expression covers every state with compiler-checked exhaustiveness. Chassis also provides various widgets for more complex scenarios.
The Quick Start walks through the rest: installation, queries, generating the mediator, and reacting to ViewModel events.
Built for the AI era
AI agents work better when code is structured into small, modular pieces with single responsibilities. Chassis enforces this structure: there is only one correct way to add a feature, and the framework is designed to keep responsibilities from leaking across layers.
Adding a feature always touches the same files in the same order: a command, a handler, a ViewModel method. Agents produce small, predictable diffs β and so do humans.
The framework ships with ready-to-use AI skills to guide agents when working with Chassis:
chassis-create-command β write operations as a
Command+CommandHandlerpairchassis-create-read-query β one-time fetches as a
ReadQuery+ReadHandlerpairchassis-create-watch-query β reactive subscriptions as a
WatchQuery+WatchHandlerpairchassis-register-handler-with-codegen β
@chassisHandlerregistration, plus every build error the generator can raisechassis-create-view-model β designing
ViewModel<State, Event>: state, events,runandwatchchassis-consume-view-model β providing and reading ViewModels:
ViewModelProvider,context.selectchassis-render-async-state β rendering
Async<T>, including anti-flicker withAsyncBuilderchassis-handle-view-model-events β one-time UI side effects: snackbars, navigation, dialogs
chassis-handle-errors β the error strategy end-to-end, from repositories to the UI
chassis-organize-feature β file layout, modules, and the multi-package split
chassis-bootstrap-app β wiring the composition root:
@ChassisApp, mediator construction, root widget treechassis-write-handler-test β unit-testing handlers with mocked repositories, no Flutter required
Install them with:
By default, skills install into .claude/skills/ for Claude Code; pass a target directory for any other agent. They come from the chassis package you depend on, so they always match your version (re-run after upgrading).
When to use Chassis
Chassis shines where architectural consistency is a primary requirement: large teams, complex applications, long-lived codebases and teams relying on AI agents, which benefit from a rigid, verifiable structure. Chassis asks for more upfront structure than unopinionated state management but it pays back in long-term maintainability.
Community & Support
Issues: GitHub Issues
Discussions: GitHub Discussions
License
Chassis is released under the MIT License. See LICENSE for details.
Last updated