I spent 3 hours writing no code — and it may have saved weeks on my ERP project

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 role field?
  • 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:

  1. Role — the user’s broad profile (admin, supervisor, operator, sales)
  2. Module — which modules the company has active
  3. 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:

  • criada
  • em_andamento
  • pausada
  • divergente
  • aguardando_aprovacao
  • concluida
  • cancelada
  • reaberta

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 I prefix
  • Supabase implementations use the Supabase prefix

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.

Leave a Reply