English Version 🇺🇸
Why I started this project
I decided to build a modular ERP as a portfolio project with two goals in mind:
- build something real, with actual business value
- learn professional software development in practice instead of studying isolated concepts with no real context
The project is called ERP Modular, and it is aimed at small businesses.
Right now, the focus is only on Module 1: Warehouse and Inventory.
The goal is not to jump into multiple modules at once. The goal is to build a solid foundation first and expand later with more confidence.
What I am building
In this first module, I want to support a real operational flow:
- import NF-e XML files, the XML documents used by Brazil's electronic invoice system
- read barcodes
- check invoice items against scanned products
- manage inventory with movement history
- generate conference PDFs
- support multiple companies with Row Level Security
So this is not just a pretty UI project. It is a system designed around real business rules.
For readers outside Brazil: NF-e ("Nota Fiscal eletrônica") is the standardized electronic invoice format widely used in the country.
Chosen stack
The stack I chose was:
- Flutter for the frontend
- Supabase for the backend
- Riverpod for state management
- Go Router for navigation
- mobile_scanner for barcode scanning
- xml for NF-e parsing
I chose Flutter because I wanted a single codebase that could eventually run on web, desktop, and mobile.
I chose Supabase because I wanted a real PostgreSQL database, built-in authentication, storage, and row-level security without having to maintain my own backend from day one.
The most important rule of the project
Before writing the first line of code, I defined one rule:
I did not want to repeat the beginner mistake of coding without direction and accumulating technical debt from the very first commit.
Because of that, my first session was entirely dedicated to planning.
I wrote no code at all. And that was intentional.
Session 1: planning before implementation
In the first session, I defined:
- the scope of the current module
- the technical stack
- the layered architecture
- the feature-based folder structure
- Git rules
- the initial database model
- the OOP concepts and design patterns I wanted to apply consciously
The architecture looked like this:
lib/
features/
estoque/
presentation/
application/
domain/
infrastructure/
notas/
presentation/
application/
domain/
infrastructure/
conferencia/
presentation/
application/
domain/
infrastructure/
core/
services/
The idea was simple:
-
presentationdisplays -
applicationorchestrates state -
domaindefines rules and contracts -
infrastructureimplements external access
I also established a few golden rules from the beginning:
- no widget talks directly to the database
- no provider knows where the data comes from
- complex objects should be created through factory constructors
- commits must follow Conventional Commits
That first session mattered because it forced me to think like someone building a real system, not just assembling screens.
Session 2: setup and infrastructure
In the second session, planning became real structure.
That was when I:
- created the public GitHub repository
- configured the project in Supabase
- generated the Flutter project correctly
- organized the layered folder structure
- added
supabase_flutter,flutter_riverpod, andgo_router - initialized Supabase in the app
- ran the application on Linux desktop
One detail that changed my mindset was the fact that the first important commit was not a feature.
It was .gitignore.
I protected credentials before almost anything else, including the local Supabase configuration file.
That may sound small, but it is not.
It was a technical decision to avoid leaking something sensitive into the repository history right at the beginning.
Another important part of this session was getting more intentional exposure to terminal commands and Git.
I had used Git before, but this was the moment when I started to better understand:
- staging
- the difference between local and remote
- commit organization
- the value of small commits
- commit history visualization in VS Code Source Control
Seeing Supabase initialized and the app running for the first time was a simple milestone, but a very meaningful one.
Session 3: full authentication and the first real engineering moment
The third session was the most intense one so far.
In about two hours, I implemented the complete authentication feature while respecting the project's layers:
- domain
- infrastructure
- application
- presentation
The final result was a real login flow with:
- email and password authentication
- form validation
- loading and error states
- display of the authenticated user's name and role
- working logout
But the most important part was not that the login worked.
It was how it was built.
1. The Usuario domain class
This was the first real domain entity in the project.
I consciously applied:
-
finalfields - a constructor with required named parameters
factory Usuario.fromMap()toMap()- equality and
hashCodeoverrides
This was the moment when some OOP concepts finally started to make sense in practice.
For example:
- immutability: an authenticated user should not suddenly become a different user during execution
- encapsulation: the object controls its own state and its own creation
2. IAuthRepository
Instead of letting the UI or the state layer talk directly to the backend, I defined an abstract contract with the main authentication methods.
In other words, anything that depends on this repository knows what it does, but not how it does it.
3. SupabaseAuthRepository
This class isolated the concrete Supabase implementation.
That taught me an important distinction:
- Supabase Auth handles credentials
- business-related user data lives in the
usuariostable
So the flow was not just “log in.”
It was:
- authenticate with Supabase Auth
- fetch the user's name, role, and company data
- transform that raw data into a valid domain object
That was one of those moments when architecture stopped being a diagram and became code with clear responsibilities.
4. AuthState and AuthNotifier
For state management, I modeled the authentication scenarios explicitly:
- initial
- loading
- authenticated
- unauthenticated
- error
That helped me understand why explicit state modeling avoids invalid combinations and makes UI logic simpler.
5. LoginScreen
The screen became responsible for observing state and displaying what made sense for each scenario.
No database logic.
No infrastructure logic.
No improper coupling.
That was exactly the kind of separation I wanted to practice in this project.
The mistake that taught me the most so far
Not everything worked on the first try.
In Session 3, I started implementing the authentication state using a Riverpod v2 pattern, but the project was already using Riverpod v3.
The result was a lot of errors at once in the editor.
It was frustrating for a few minutes, but it turned into one of the best lessons so far.
Because the problem was not simply “the code is broken.”
The problem was more professional than that:
I was using an outdated pattern for a different version of the library.
Fixing it required rewriting the implementation using Notifier and NotifierProvider, and that made the concept much clearer than if everything had worked immediately.
Another small but useful mistake also happened: I committed an empty interface file because I forgot to save it before running git add.
I fixed it with a fix commit and enabled autosave to prevent it from happening again.
What this project is actually teaching me
So far, the biggest gain is not only technical.
It is mindset.
I am starting to understand more clearly:
- why architecture is not bureaucracy
- why separating responsibilities reduces confusion
- why small commits improve the way I think
- why patterns make sense when they emerge as solutions to real problems
- why documenting blockers is just as valuable as documenting wins
I am also seeing something important in practice:
good code does not appear ready-made.
It improves as the foundation, criteria, and understanding improve.
Next steps
The next focus of the project is to keep expanding the Warehouse and Inventory module according to the roadmap, moving toward product listing screens, product modeling, XML import, barcode-based checking, stock movements, and reports.
But the priority remains the same:
grow with consistency, not in a hurry.
Conclusion
This project is my way of studying software development more honestly.
Instead of only consuming theory, I am trying to build something real and use every step to better understand:
- OOP
- layered architecture
- design patterns
- Git
- domain modeling
- backend integration
It is still the beginning.
But it is the most solid beginning I have managed to build so far.
If you are also studying architecture, Flutter, or building a portfolio project with more intention than urgency, I will keep documenting this journey.
Versão em Português 🇧🇷
Por que comecei este projeto
Decidi construir um ERP modular como projeto de portfólio com dois objetivos em mente:
- construir algo real, com valor de negócio de verdade
- aprender desenvolvimento de software de forma prática, em vez de estudar conceitos isolados sem contexto real
O projeto se chama ERP Modular e é voltado para pequenos negócios.
Neste momento, o foco está somente no Módulo 1: Almoxarifado e Estoque.
A ideia não é sair construindo vários módulos de uma vez. A prioridade é criar uma base sólida primeiro e expandir depois com mais confiança.
O que estou construindo
Neste primeiro módulo, quero atender um fluxo operacional real:
- importar arquivos XML de NF-e
- ler códigos de barras
- conferir itens da nota com os produtos escaneados
- controlar o estoque com histórico de movimentações
- gerar PDFs de conferência
- suportar múltiplas empresas com Row Level Security
Ou seja, este não é apenas um projeto com uma interface bonita. É um sistema pensado em torno de regras de negócio reais.
Stack escolhida
A stack que defini foi:
- Flutter no frontend
- Supabase no backend
- Riverpod para gerenciamento de estado
- Go Router para navegação
- mobile_scanner para leitura de códigos de barras
- xml para parse de NF-e
Escolhi Flutter porque queria um único codebase com potencial para rodar em web, desktop e mobile.
Escolhi Supabase porque queria um banco PostgreSQL real, autenticação pronta, storage e segurança em nível de linha sem precisar manter um backend próprio desde o primeiro dia.
A regra mais importante do projeto
Antes de escrever a primeira linha de código, defini uma regra:
eu não queria repetir o erro de iniciante de sair codando sem direção e acumular dívida técnica desde o primeiro commit.
Por isso, minha primeira sessão foi inteiramente dedicada ao planejamento.
Eu não escrevi nenhuma linha de código. E isso foi intencional.
Sessão 1: planejamento antes da implementação
Na primeira sessão, defini:
- o escopo do módulo atual
- a stack técnica
- a arquitetura em camadas
- a estrutura de pastas por feature
- as regras de Git
- a modelagem inicial do banco
- os conceitos de POO e os design patterns que eu queria aplicar conscientemente
A arquitetura ficou assim:
lib/
features/
estoque/
presentation/
application/
domain/
infrastructure/
notas/
presentation/
application/
domain/
infrastructure/
conferencia/
presentation/
application/
domain/
infrastructure/
core/
services/
A lógica era simples:
-
presentationexibe -
applicationorquestra estado -
domaindefine regras e contratos -
infrastructureimplementa o acesso externo
Também estabeleci algumas regras de ouro desde o começo:
- nenhum widget fala diretamente com o banco de dados
- nenhum provider sabe de onde os dados vêm
- objetos complexos devem ser criados por factory constructors
- os commits precisam seguir Conventional Commits
Essa primeira sessão foi importante porque me obrigou a pensar como alguém que está construindo um sistema real, e não apenas juntando telas.
Sessão 2: setup e infraestrutura
Na segunda sessão, o planejamento virou estrutura real.
Foi quando eu:
- criei o repositório público no GitHub
- configurei o projeto no Supabase
- gerei o projeto Flutter corretamente
- organizei a estrutura de pastas da arquitetura
- adicionei
supabase_flutter,flutter_riverpodego_router - inicializei o Supabase no app
- rodei a aplicação no Linux desktop
Um detalhe que mudou minha forma de pensar foi o fato de o primeiro commit importante não ter sido uma feature.
Foi o .gitignore.
Eu protegi as credenciais antes de quase qualquer outra coisa, incluindo o arquivo local de configuração do Supabase.
Isso pode parecer pequeno, mas não é.
Foi uma decisão técnica para evitar que algo sensível vazasse para o histórico do repositório logo no começo.
Outra parte importante dessa sessão foi ter um contato mais intencional com comandos de terminal e com Git.
Eu já tinha usado Git antes, mas esse foi o momento em que comecei a entender melhor:
- staging
- a diferença entre local e remoto
- a organização dos commits
- o valor de commits pequenos
- a visualização do histórico de commits no Source Control do VS Code
Ver o Supabase inicializado e o app rodando pela primeira vez foi um marco simples, mas muito significativo.
Sessão 3: autenticação completa e o primeiro momento real de engenharia de software
A terceira sessão foi a mais intensa até agora.
Em cerca de duas horas, implementei a feature completa de autenticação respeitando as camadas do projeto:
- domain
- infrastructure
- application
- presentation
O resultado final foi um fluxo de login real com:
- autenticação por e-mail e senha
- validação de formulário
- estados de carregamento e erro
- exibição do nome e do papel do usuário autenticado
- logout funcionando
Mas a parte mais importante não foi apenas o login funcionar.
Foi como ele foi construído.
1. A classe de domínio Usuario
Essa foi a primeira entidade de domínio real do projeto.
Nela, apliquei conscientemente:
- campos
final - construtor com parâmetros nomeados obrigatórios
factory Usuario.fromMap()toMap()- sobrescrita de igualdade e
hashCode
Esse foi o momento em que alguns conceitos de POO finalmente começaram a fazer sentido na prática.
Por exemplo:
- imutabilidade: um usuário autenticado não deve simplesmente virar outro durante a execução
- encapsulamento: o objeto controla o próprio estado e a própria criação
2. IAuthRepository
Em vez de deixar a UI ou a camada de estado falarem diretamente com o backend, defini um contrato abstrato com os principais métodos de autenticação.
Em outras palavras, qualquer parte que dependa desse repositório sabe o que ele faz, mas não como ele faz.
3. SupabaseAuthRepository
Essa classe isolou a implementação concreta com Supabase.
Isso me ensinou uma distinção importante:
- o Supabase Auth cuida das credenciais
- os dados de negócio do usuário ficam na tabela
usuarios
Então o fluxo não era apenas “fazer login”.
Era:
- autenticar com o Supabase Auth
- buscar nome, papel e dados da empresa do usuário
- transformar esses dados brutos em um objeto de domínio válido
Esse foi um daqueles momentos em que a arquitetura deixou de ser um diagrama e virou código com responsabilidades claras.
4. AuthState e AuthNotifier
No gerenciamento de estado, modelei explicitamente os cenários da autenticação:
- inicial
- carregando
- autenticado
- não autenticado
- erro
Isso me ajudou a entender por que uma modelagem explícita de estados evita combinações inválidas e simplifica a lógica da UI.
5. LoginScreen
A tela passou a ser responsável por observar o estado e exibir o que fazia sentido em cada cenário.
Sem lógica de banco de dados.
Sem lógica de infraestrutura.
Sem acoplamento indevido.
Esse era exatamente o tipo de separação que eu queria praticar neste projeto.
O erro que mais me ensinou até agora
Nem tudo funcionou de primeira.
Na Sessão 3, comecei implementando o estado da autenticação com um padrão de Riverpod v2, mas o projeto já estava usando Riverpod v3.
O resultado foi uma grande quantidade de erros de uma vez no editor.
Foi frustrante por alguns minutos, mas acabou se tornando uma das melhores lições até aqui.
Porque o problema não era simplesmente “o código está quebrado”.
O problema era mais profissional do que isso:
eu estava usando um padrão desatualizado para uma versão diferente da biblioteca.
Corrigir isso exigiu reescrever a implementação usando Notifier e NotifierProvider, e isso deixou o conceito muito mais claro do que se tudo tivesse funcionado de primeira.
Outro erro pequeno, mas útil, também aconteceu: comitei um arquivo de interface vazio porque esqueci de salvá-lo antes de executar git add.
Corrigi com um commit de fix e ativei o autosave para evitar que isso aconteça de novo.
O que este projeto está realmente me ensinando
Até aqui, o maior ganho não é apenas técnico.
É mentalidade.
Estou começando a entender com mais clareza:
- por que arquitetura não é burocracia
- por que separar responsabilidades reduz confusão
- por que commits pequenos melhoram a forma como eu penso
- por que patterns fazem sentido quando surgem como solução para problemas reais
- por que documentar travamentos é tão valioso quanto documentar acertos
Também estou vendo algo importante na prática:
código bom não aparece pronto.
Ele melhora à medida que a base, os critérios e o entendimento evoluem.
Próximos passos
O próximo foco do projeto é continuar expandindo o módulo de Almoxarifado e Estoque de acordo com o cronograma, avançando para telas de listagem de produtos, modelagem de produto, importação de XML, conferência por código de barras, movimentações de estoque e relatórios.
Mas a prioridade continua a mesma:
crescer com consistência, não com pressa.
Conclusão
Este projeto é a minha forma de estudar desenvolvimento de software de maneira mais honesta.
Em vez de apenas consumir teoria, estou tentando construir algo real e usar cada etapa para entender melhor:
- POO
- arquitetura em camadas
- design patterns
- Git
- modelagem de domínio
- integração com backend
Ainda é só o começo.
Mas é o começo mais sólido que já consegui construir até aqui.
Se você também está estudando arquitetura, Flutter ou construindo um projeto de portfólio com mais intenção do que urgência, eu vou continuar documentando essa jornada.
Top comments (0)