Motivação do estudo
O objetivo do StockFlow é entender, na prática, como amarrar Kubernetes, mensageria assíncrona e persistência num sistema com forma real de produção — sem se distrair com um domínio de negócio complexo. O domínio (produtos + movimentações de estoque) é deliberadamente trivial: o que importa aqui não é a regra de negócio, é a infraestrutura ao redor dela.
Duas perguntas guiam o estudo. Primeira: como provar que pub/sub via RabbitMQ realmente cruza um
processo — não um “consumer” fake rodando dentro do mesmo app, mas dois binários .NET distintos, cada
um com sua própria conexão com o broker e com o Postgres, falando só através de uma fila. Segunda: como
esse conjunto de serviços — API, worker, banco, broker, frontend — se descreve em manifests Kubernetes
que alguém consegue ler e entender, sem a abstração extra de um chart Helm por cima.
Autenticação usa JWT próprio (Users + BCrypt + HS256), não Authentik — o AuthStudy já
estuda OIDC/Flow-Executor em profundidade; repetir isso aqui só diluiria o assunto real deste repositório.
O que foi feito
- Dois processos .NET 10 desacoplados:
StockFlow.Api(minimal API — HTTP, leitura/escrita no Postgres, publica eventos) eStockFlow.Worker(BackgroundServiceque só consome RabbitMQ e escreve notificações). A divisão existe justamente pra provar que o pub/sub atravessa um limite de processo de verdade. - Topic exchange, não fila-por-evento: toda movimentação publica
stock.level.changed; quando o resultado cruza o limite mínimo, publica tambémstock.low-threshold. Só existe uma fila real hoje (stockflow.low-stock-alerts), mas o exchange topic deixa claro que adicionar um segundo consumidor (ex.: um serviço de auditoria ligado emstock.#) é só uma fila + binding nova, não uma mudança no publisher. - Auth JWT real, não stub: registro/login com BCrypt, token HS256 de 2h validado via
AddJwtBearer, protegendo os endpoints de produtos e movimentações. - Notifications como tabela cross-service: o schema é dono da API (suas migrations), mas o Worker
escreve nela com seu próprio
DbContext, leve e sem migrations — prova que o Worker é um serviço independente de verdade, com sua própria conexão ao Postgres, não “a API com outro entrypoint”. - Publish best-effort, não outbox: a movimentação de estoque commita no Postgres primeiro; o publish no RabbitMQ roda depois, num try/catch que loga mas não desfaz a escrita. Um outage do broker nunca derruba um dado de estoque já commitado — mas o evento pode ser perdido silenciosamente. Isso é documentado como gap conhecido, não escondido.
- Kubernetes com Kustomize puro: manifests YAML legíveis (
namespace,Secret, Postgres, RabbitMQ, API, Worker, frontend) sem camada de templating Helm — o ponto do repo é dar pra ler o YAML e entender exatamente o que sobe. - Frontend React com painel de alertas ao vivo: login/registro, lista + criação de produtos,
formulário de movimentação, e um painel que faz polling em
/api/notificationsa cada 5s — prova visual de que a escrita do worker realmente aparece do outro lado.
Como funciona
Visão geral do sistema
flowchart TB
Browser["🌐 Navegador (usuário)"]
FE["frontend :5173 — React + Vite + Tailwind\nlogin · produtos · movimentações · alertas"]
subgraph Backend[".NET 10"]
Api["api :8080\nJWT auth · Products · Movements · publica eventos"]
Worker["worker\nconsome low-stock events → notifications"]
end
PG[("postgres\nUsers · Products · StockMovements · Notifications")]
subgraph MQ["RabbitMQ"]
Exchange{{"stockflow.stock-events\n(topic exchange)"}}
Queue[["stockflow.low-stock-alerts\n(fila durável)"]]
end
Browser --> FE
FE -->|"REST + Bearer JWT"| Api
Api --> PG
Api -->|"publica: stock.level.changed\nstock.low-threshold"| Exchange
Exchange -->|"bind: stock.low-threshold"| Queue
Queue --> Worker
Worker --> PG
Autenticação: registro e login com JWT próprio
sequenceDiagram
actor U as Usuário (navegador)
participant FE as frontend
participant Api as api
participant PG as postgres
U->>FE: preenche email + senha, envia registro
FE->>Api: POST /api/auth/register
Api->>Api: valida email/senha (min. 6 chars)
Api->>PG: SELECT Users WHERE Email = ?
alt email já existe
PG-->>Api: encontrado
Api-->>FE: 409 Conflict
else email livre
Api->>Api: BCrypt.HashPassword(senha)
Api->>PG: INSERT User
PG-->>Api: commit
Api->>Api: gera JWT HS256 (claims: sub, email; exp 2h)
Api-->>FE: 200 { token, email }
end
FE->>FE: guarda token, navega pro dashboard
Note over U,PG: Login segue o mesmo par request/response
U->>FE: envia email + senha
FE->>Api: POST /api/auth/login
Api->>PG: SELECT User WHERE Email = ?
Api->>Api: BCrypt.Verify(senha, hash)
alt credenciais válidas
Api-->>FE: 200 { token, email }
else inválidas
Api-->>FE: 401 Unauthorized
end
Fluxo de evento de estoque — RabbitMQ pub/sub ponta a ponta
sequenceDiagram
actor U as Usuário (navegador)
participant FE as frontend
participant Api as api
participant PG as postgres
participant MQ as RabbitMQ (topic exchange)
participant W as worker
U->>FE: registra movimentação (Out, qtd 7)
FE->>Api: POST /api/products/{id}/movements (Bearer JWT)
Api->>PG: UPDATE Product.Quantity, INSERT StockMovement
PG-->>Api: commit
Api->>MQ: publica "stock.level.changed" (StockLevelChanged)
alt quantidade resultante <= LowStockThreshold
Api->>MQ: publica "stock.low-threshold" (LowStockThresholdReached)
end
Note over Api,MQ: publish é best-effort (try/catch) — a escrita\nno postgres já commitou antes e nunca é desfeita
Api-->>FE: 201 Created { resultingQuantity }
MQ->>W: entrega em stockflow.low-stock-alerts\n(bound à routing key stock.low-threshold)
W->>PG: INSERT Notification (DbContext próprio do worker)
W->>W: loga "LOW STOCK ALERT: ..."
W-->>MQ: ack
loop a cada 5s
FE->>Api: GET /api/notifications (polling)
Api->>PG: SELECT Notifications
Api-->>FE: [{ message, createdAt, ... }]
end
FE-->>U: painel de alertas atualiza
Modelo de dados
Notification não tem FK pra Product de propósito — quem escreve nela é um processo diferente
(StockFlow.Worker) do que é dono de Product (StockFlow.Api), o que mantém honesto o limite entre
os dois acessos ao banco.
erDiagram
PRODUCT ||--o{ STOCK_MOVEMENT : tem
USER {
guid Id PK
string Email
string PasswordHash "BCrypt"
datetimeoffset CreatedAt
}
PRODUCT {
guid Id PK
string Sku
string Name
int Quantity
decimal Price
int LowStockThreshold
}
STOCK_MOVEMENT {
guid Id PK
guid ProductId FK
string Type "In | Out"
int Quantity
string Note
datetimeoffset CreatedAt
}
NOTIFICATION {
guid Id PK
guid ProductId "sem FK — escrita por outro serviço"
string Sku
string Message
datetimeoffset CreatedAt
}
Kubernetes: como o deploy é organizado
Manifests puros em k8s/base/ com Kustomize (sem Helm) — sete recursos aplicados como uma unidade
(kubectl apply -k k8s/base): namespace, Secret (credenciais em stringData, plaintext — aceitável
num repo de estudo, nunca em produção), postgres e rabbitmq (cada um com PVC + Deployment + Service),
api e worker (Deployments consumindo a mesma Secret), e frontend (Deployment + ClusterIP +
NodePort em 30080 pra acesso local via kind/minikube/Docker Desktop). Validado estaticamente com
kubectl kustomize + kubectl apply --dry-run=client — não implantado ao vivo neste ambiente pra não
disputar recursos com um cluster compartilhado já em uso por outros projetos; os manifests estão prontos
pra kubectl apply -k k8s/base num cluster dedicado.
Stack
- ◆ BACKEND: .NET 10 minimal APIs (
StockFlow.Api+StockFlow.Worker), EF Core + Npgsql, migrations aplicadas no boot. - ✉ MENSAGERIA: RabbitMQ — topic exchange
stockflow.stock-events, fila durávelstockflow.low-stock-alerts, cliente oficialRabbitMQ.Clientassíncrono. - ⛁ DADOS: PostgreSQL — um schema por serviço (Api dono das migrations; Worker com
DbContextpróprio, sem migrations). - ⛨ AUTH: JWT próprio — BCrypt para hash de senha, HS256, token de 2h sem refresh.
- ◈ FRONTEND: React + Vite + Tailwind v4 — login, CRUD de produtos, movimentações, painel de alertas com polling.
- ⌂ INFRA: Docker Compose (inner loop local) + Kubernetes via Kustomize puro (
k8s/base/, sem Helm).
Como rodar
# Docker Compose — caminho principal, verificado ponta a ponta
cp .env.example .env
docker compose up --build
# Postgres :5433 · RabbitMQ :5673 (management UI :15672) · API :5090 · frontend :5173
# Kubernetes — manifests prontos, validados estaticamente
docker build -t stockflow-api:local -f src/StockFlow.Api/Dockerfile .
docker build -t stockflow-worker:local -f src/StockFlow.Worker/Dockerfile .
docker build -t stockflow-frontend:local ./frontend
MSYS_NO_PATHCONV=1 kubectl apply -k k8s/base
kubectl -n stockflow get pods -w
# frontend exposto via NodePort 30080
kubectl -n stockflow get svc frontend-nodeport # Código aberto
O StockFlow é open source, feito pra estudar e servir de referência de infraestrutura.