Why this session mattered
Session 4 of my ERP Modular project had no code at all.
No UI.
No new repository.
No new feature branch with a working screen.
And it may still have been one of the most important sessions so far.
Instead of implementing the next feature immediately, I spent about three hours reviewing the project structure, identifying architectural gaps, and formalizing decisions that would affect every module from this point on.
That changed the nature of the project.
It stopped being just a technically planned app and started becoming a structured product architecture.
The context
ERP Modular is an open source portfolio project I am building with:
- Flutter
- Supabase
- Riverpod v3
- GoRouter
The current focus is still Module 1: Warehouse and Inventory.
The first three sessions had already covered:
- project planning
- setup and infrastructure
- full authentication with layered architecture
By Session 4, the codebase already had a foundation. But the architectural review showed that some important questions had not yet been fully decided:
- What should happen right after login?
- How should permissions work beyond a simple
rolefield? - Should stock be controlled only per product, or per product and lot?
- How should invoice duplication be handled?
- What conventions should remain stable across future modules?
- How should the system represent errors consistently?
Those are exactly the kinds of decisions that often get postponed until the code starts hurting.
I wanted to make them before that happened.
What was defined in Session 4
1. A real ERP entry point instead of jumping straight into Inventory
One of the first decisions was to stop thinking of the app as “login -> inventory”.
That would not fit a modular ERP.
So the post-login entry point became a general ERP home screen, with module cards shown according to:
- what the company has activated
- what the user has permission to access
That seems small, but it changes the entire navigation structure.
Instead of a feature-first entry point, the app now has a product-level entry point.
2. Permissions in three layers
A single role field would not be enough.
So the permission model was defined in three layers:
-
Role — the user's broad profile (
admin,supervisor,operator,sales) - Module — which modules the company has active
-
Action — what the user can do inside each module (
view,import,approve,edit, etc.)
This matters because real systems rarely map cleanly to a single role.
For example, two users can both be operators while still having different permissions inside Inventory.
The verification strategy was also defined in three places:
- GoRouter for route-level redirection
- widgets for UI visibility and usability
- RLS in Supabase for actual data protection
That last part is important: hiding a button is not security.
3. Product and stock modeling became much more realistic
The product model was expanded well beyond the original basic structure.
It now includes concepts such as:
- multiple barcodes
- NCM and CEST fields
- physical location
- image URL
- cost and sale price
- reprocessing support
- lot control
- expiry date
The biggest modeling decision was this:
stock will be tracked by product and by lot from the beginning.
That has a major impact on the domain and database design, but it avoids a very common problem: starting with a simplified stock model and later discovering that the business flow actually needs traceability.
In this project, traceability matters.
That means the system needs to know:
- total stock for a product
- current stock for a specific lot
- lot origin in reprocessing scenarios
- which lot was used in each movement
That is much better to decide now than to retrofit later.
4. Invoice duplication is no longer a vague edge case
Another important decision was around duplicate NF-e imports.
For readers outside Brazil: NF-e (“Nota Fiscal eletrônica”) is the standardized electronic invoice format widely used in the country, and these invoices are often processed through XML files.
The system now has a defined duplication strategy:
- detect duplicates by the invoice access key
- show the existing record and its current status
- allow navigation to the original note
- allow reimport only with a required reason and supervisor approval
- register the exception separately
A unique constraint on chave_acesso in the database was also defined to prevent duplication even if the Flutter logic fails.
That is one of my favorite types of decision: protect the flow both in the app and in the database.
5. Conference is now treated as a real state machine
This was one of the biggest improvements in the session.
The conference flow is no longer just “open” or “closed”.
It now has an explicit state machine:
criadaem_andamentopausadadivergenteaguardando_aprovacaoconcluidacanceladareaberta
More important than the list of states were the rules around the transitions.
For example:
- any operator can pause and resume a conference
- a divergent conference requires supervisor approval with a reason
- reopening a completed conference also requires supervisor approval and a reason
This is exactly the kind of business rule that becomes fragile when modeled with booleans.
A state machine makes it explicit.
6. Naming conventions and error handling were standardized
Two technical patterns became formal rules during this session.
Naming
The project now has a definitive naming convention for:
- database tables
- Dart classes
- interfaces
- repository implementations
- files and folders
For example:
- database tables use plural snake_case
- Dart classes use singular PascalCase
- interfaces use the
Iprefix - Supabase implementations use the
Supabaseprefix
This sounds boring until the project starts growing.
At that point, inconsistent names become friction everywhere.
Error handling with Resultado<T>
This was another major decision.
Repositories will no longer throw raw exceptions to the rest of the app.
Instead, they return a sealed result type:
Sucesso<T>Falha<T>
And failures are classified by a TipoFalha enum, with cases such as:
- validation
- domain
- permission
- not found
- duplicate
- invalid XML
- network
- server
- unknown
The rule by layer became:
-
repository returns
Resultado<T> - notifier translates that into state
- widget displays state without knowing the failure type directly
That is one of the cleanest architectural decisions made so far.
7. A universal module guide was created
The final big outcome of the session was the creation of a Universal Module Guide.
The point of that document is simple:
I do not want to re-decide the same architectural rules every time a new module is created.
So instead of having architecture spread across memory, old chats, and assumptions, the project now has a reusable reference that defines:
- mandatory folder structure
- quality checklist
- rules shared by all modules
- module lifecycle
- things that should never be done
That means future modules such as Sales, Technical Support, or Dashboards can start from a defined standard.
What I learned from a session with no code
This session reinforced something I am starting to value more and more:
architecture is not a delay before coding. It is one of the ways you avoid writing the wrong code faster.
It also reminded me that a good architecture review does not need to happen only inside one tool or from one perspective.
This session used both Claude and ChatGPT as structured reviewers for the same set of documents.
The result was surprisingly useful.
Not because either one “solved the architecture”, but because they pushed the review from different angles:
- one helped consolidate decisions
- the other helped identify missing points and inconsistencies
That process felt very close to a lightweight version of peer review.
What comes next
The next session returns to implementation.
The immediate goal is to replace home: LoginScreen in main.dart with:
- GoRouter
- ShellRoute
- a responsive AppShell
- a general HomeScreen with module cards
- authentication-based redirection
But now that code will be written on top of a much clearer architectural base.
Final thought
A lot of beginner portfolio projects try to prove skill by showing how many features were shipped quickly.
I am trying something different.
I want this project to show that I am learning how to make technical decisions with intention.
Sometimes that means writing code.
Sometimes it means spending three hours writing no code at all — and still moving the project forward in a way that matters.
Top comments (0)