// ESTUDO · INFRAESTRUTURA DE MENSAGERIA E DEPLOY · OPEN SOURCE

StockFlow

Boilerplate de referência pra colar Kubernetes, RabbitMQ e PostgreSQL num sistema real e implantável: API em .NET 10 publica eventos de estoque num topic exchange, um worker separado consome e grava notificações, e um frontend React expõe tudo — do domínio de estoque, deliberadamente trivial, só importa a infra em volta.

OPEN SOURCE KUBERNETESRABBITMQPOSTGRESQL.NET 10JWT AUTH

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

  1. Dois processos .NET 10 desacoplados: StockFlow.Api (minimal API — HTTP, leitura/escrita no Postgres, publica eventos) e StockFlow.Worker (BackgroundService que 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.
  2. Topic exchange, não fila-por-evento: toda movimentação publica stock.level.changed; quando o resultado cruza o limite mínimo, publica também stock.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 em stock.#) é só uma fila + binding nova, não uma mudança no publisher.
  3. 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.
  4. 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”.
  5. 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.
  6. 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.
  7. 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/notifications a 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ável stockflow.low-stock-alerts, cliente oficial RabbitMQ.Client assíncrono.
  • ⛁ DADOS: PostgreSQL — um schema por serviço (Api dono das migrations; Worker com DbContext pró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.