Template para criar modulos para o Backoffice Shell. Clone, desenvolva, deploy — o modulo se registra automaticamente no shell.
# 1. "Use this template" no GitHub, ou clone:
git clone https://github.com/corabank/platform-module-quickstart my-module
cd my-module
# 2. Instalar dependencias (frontend + backend)
npm install
(cd backend && npm install)
# 3. Dev server — frontend + backend em paralelo
npm run dev:all # frontend :4100, backend :8080
# 4. Testes
npm test
(cd backend && npm test)
# 5. Build
npm run build # → dist/module.js
(cd backend && npm run build) # → backend/dist/server.jsStack default: React + Vite no
src/+ Hono nobackend/. Se quiser trocar a stack do backend por Go/Python/Kotlin, rode/switch-backend-stack <stack>. Se seu modulo e pure-view e nao precisa de backend, rode/remove-backend.
Seu modulo e carregado pelo shell em tempo de execucao. Voce desenvolve de forma independente no seu proprio repo.
Seu repo Shell
┌──────────────┐ deploy ┌──────────────────┐
│ src/ │ ──────────→ │ Carrega via │
│ module.json │ CDN/K8s │ import(bundleUrl) │
│ dist/ │ │ │
└──────────────┘ └──────────────────┘
│ │
└─── auto-registro ───────────→ │
(POST /api/modules) ↓
Aparece na sidebar
| Feature | Como usar |
|---|---|
| Usuario | context.user |
| Permissoes | context.hasPermission("orders", "write") |
| Auditoria | context.auditLog("order.created", { id: "123" }) |
| Notificacoes | context.notify("Salvo!", "success") |
| Cross-module | context.eventBus.emit("order.created", data) |
| Logger | context.logger.info("processando", { orderId }) |
| Tema | CSS vars automaticos + useShellTheme(context) |
| Cache compartilhado | context.sharedCache.get/set/invalidate/subscribe |
| Token OAuth | context.getProviderToken() ou useApi(context) |
src/ # Frontend (React + Vite)
├── index.ts # Entry point (bootstrap, mount, unmount)
├── register.ts # Auto-registro com o shell
├── components/
│ ├── App.tsx # Componente principal
│ └── ExampleBackendCall.tsx # Referencia do padrao front→back→API
├── hooks/
│ ├── useShellTheme.ts # Reagir a mudanca de tema
│ ├── useApi.ts # Chamadas autenticadas genericas
│ └── useBackendApi.ts # Chamadas ao backend deste modulo
├── styles/
│ └── module.css # Estilos com design tokens
└── test-utils.ts # Mock do ShellContext
backend/ # Backend (Hono + Node)
├── src/
│ ├── server.ts # Hono app, rotas sob /api
│ ├── config.ts # Env vars
│ └── routes/
│ └── example.ts # Referencia de proxy para API externa
├── package.json
├── tsconfig.json
└── Dockerfile
charts/my-module/ # Helm chart (frontend + backend, 1 release)
platform/configs/ # Values por ambiente (stage, prod)
module.json # Metadados para auto-registro
Cada módulo aparece no grid da Home do shell como um card com um chip colorido (44×44) e um ícone. Dois campos opcionais no module.json controlam isso:
"accent": "coral",
"icon": "shield",accent — cor do chip. Um destes 7 valores:
| accent | hue (aprox.) | uso típico |
|---|---|---|
coral |
vermelho coral | ações principais / regulatório |
amber |
âmbar | configuração / toggles |
violet |
roxo | orquestração / fluxo |
teal |
verde-azul | aprendizagem / coleta |
blue |
azul | ferramentas / API |
rose |
rosa | suporte / pessoas |
lime |
verde-lima | dados / analytics |
icon — nome do ícone lucide em kebab-case (ex.: shield, git-branch, database, book-open, key).
Se você não declarar accent, o shell pega 1 dos 7 valores via hash determinístico do id do módulo — mesmo id sempre cai na mesma cor, em qualquer sessão e dispositivo.
Se você não declarar icon, o shell usa um default por group:
| group | icon default |
|---|---|
operations |
layers |
finance |
wallet |
customer-support |
life-buoy |
analytics |
chart-line |
settings |
settings |
Sem custo cognitivo no dia 1 — declare quando quiser dar identidade visual ao módulo, deixe vazio quando estiver tudo bem com o auto.
A skill /check-module valida se o accent declarado está no enum e avisa quando os dois estão ausentes (informativo — não bloqueia deploy).
Por padrão o module.json declara um menu único no campo navigation (caso da maioria dos módulos):
"navigation": [
{ "label": "Overview", "path": "/my-module", "requiredPermission": "my-module:read" },
{ "label": "Items", "path": "/my-module/items", "requiredPermission": "my-module:read" }
]Se o módulo precisa expor menus diferentes para personas distintas do mesmo usuário (ex.: "Colaborador" vs "Líder" num módulo de despesas), use perspectives no lugar de navigation:
"perspectives": [
{
"id": "colaborador",
"label": "Colaborador",
"navigation": [
{ "label": "Meu Painel", "path": "/my-module" },
{ "label": "Minhas Solicitações", "path": "/my-module/solicitacoes" }
]
},
{
"id": "lider",
"label": "Líder",
"navigation": [
{ "label": "Painel do Líder", "path": "/my-module/painel-lider", "requiredPermission": "my-module:approve" },
{ "label": "Histórico do Time", "path": "/my-module/time", "requiredPermission": "my-module:approve" }
]
}
]O shell renderiza um combobox no topo do sidebar quando há ≥2 perspectivas visíveis ao usuário e troca o menu na seleção. A escolha persiste no backend (segue o user entre dispositivos).
Regras (validadas no POST /api/modules — manifest fora do schema é rejeitado):
navigationeperspectivessão mutuamente exclusivos nomodule.json. Declare um ou outro, nunca os dois.- Cada perspectiva precisa de
idúnico,labelnão-vazio e ao menos um item emnavigation. - Visibilidade é derivada do nav: a perspectiva aparece para o user quando pelo menos um item da nav dela é visível (não há
requiredPermissionna própria perspectiva). - Paths seguem o mesmo padrão do
navigationflat: o convencional nomodule.jsoné absoluto começando com/${id}/...(como nos exemplos acima), e o Shell API normaliza para a forma relativa interna no registro. Forma relativa direta ("","items") também é aceita. O que não vale é drift —/foonum módulobaré rejeitado.
Validação local: rode
/check-moduleantes de subir mudanças nomodule.jsonpara pegar erros que o Shell API rejeitaria no deploy.
Regra de ouro: o frontend nunca chama API externa direto. Toda chamada pra banco/backoffices/APIs de terceiros passa pelo backend deste modulo, que atua como BFF/proxy. Ver CLAUDE.md pra contexto completo.
import { useBackendApi } from "@/hooks/useBackendApi";
function OrderList({ context }) {
const api = useBackendApi(context);
const orders = await api.get("/orders");
// Em dev: http://localhost:8080/api/orders
// Em prod: /modules/{module-id}/api/orders (via shell proxy)
}Editar backend/src/routes/ e adicionar a chamada com fetch. Credenciais vem de env vars (Vault via ExternalSecret em prod). Ver backend/src/routes/example.ts como referencia.
Se o servico externo aceita o token OIDC do proprio user (mesmo realm Keycloak), da pra usar useApi sem backend — mas audit, cache e transformacao ficam espalhados. Prefira backend sempre que possivel.
Use CSS Custom Properties — adaptam automaticamente:
color: var(--foreground);
background: var(--primary); /* azul Cora */
border: 1px solid var(--border);Nunca hardcode cores. Tokens disponiveis: --primary, --foreground, --muted-foreground, --border, --cora-positive, --cora-negative, --cora-alert, --cora-info (cada um com -foreground).
O modulo se registra sozinho quando sobe:
App inicia → healthy → le module.json → POST /api/modules → shell ativa
Heartbeat 30s → mantem ativo | SIGTERM → desativa
Env vars no deploy:
SHELL_API_URL=https://backoffice.stage.cora.team
SHELL_TOKEN=<token-de-servico>
BUNDLE_URL=https://cdn.internal/modules/meu-modulo/1.0.0/module.jsSem estas vars, auto-registro e ignorado (seguro para dev local).
npm run build→dist/module.js- Deploy no CDN
- Shell sidebar → Import Module → informar URL
| Comando | Descricao |
|---|---|
npm run dev |
Frontend dev server (4100) |
npm run dev:backend |
Backend dev server (8080) |
npm run dev:all |
Frontend + backend em paralelo |
npm run build |
Build frontend (dist/module.js) |
npm test |
Testes frontend (watch) |
npm run test:run |
Testes frontend 1x |
npm run test:coverage |
Cobertura frontend |
npm run lint |
ESLint |
npm run typecheck |
TypeScript (frontend) |
cd backend && npm test |
Testes backend |
cd backend && npm run typecheck |
TypeScript (backend) |
- Todo mutation →
context.auditLog()+context.sharedCache.invalidate()— sem exceção - Feedback →
context.notify()— nuncaalert() - Logs →
context.logger— nuncaconsole.log - Cores → CSS vars — nunca hardcodar
- TDD — 80% cobertura minima
- Max 200 linhas/arquivo, 50 linhas/funcao