Skip to content

Micro-apps plugáveis por comunidade com catálogo estático + enablement por tenant

Fase 1 do sistema de micro-apps: cada comunidade pode habilitar/desabilitar e configurar ferramentas pequenas e autocontidas (começando pelo metrônomo). O que um micro-app é vive num catálogo estático em Python (single source of truth); se/quando/como um tenant o usa vive em rows CommunityMicroApp. A frente lazy-carrega um componente por app_id via @repo/micro-apps.

Contexto

O produto é multi-tenant por nicho (música). Os tenants querem ferramentas diferentes habilitadas e configuradas de forma diferente — um metrônomo é o caso canônico (BPM padrão, compasso, acento), e o nicho pede mais por vir (afinador, transpositor de acordes, ...). Três arquiteturas foram consideradas:

  1. Plugin runtime (carregar código arbitrário por tenant) — descartado: é a mais flexível mas traz superfície de segurança/confiabilidade enorme (código não confiável, isolamento, versionamento).
  2. Micro-frontends — descartado: overhead de infra (exposição, auth, build) desproporcional para ferramentas pequenas; o custo não paga o isolamento que não precisamos.
  3. Catálogo estático + enablement por tenant + componentes lazy no frontendescolhida. O catálogo Python é o único lugar que define "o que existe" (nome, descrição, categoria, schema de config); CommunityMicroApp (uma row por community+app_id) só registra is_enabled, config (JSON de overrides) e order. Adicionar um micro-app = uma entry no catálogo + um componente lazy no registry do frontend, ambos chaveados pelo mesmo app_id. Nada de carregamento dinâmico de plugin, nada de DB definindo comportamento.

Decisão

  • Catálogo estático: backend/apps/communities/micro_apps.pyMICRO_APPS: dict[str, MicroAppDefinition], cada definição com app_id, name, description, category, config_schema (shape JSON-schema minimal) e default_config. validate_config(app_id, config) valida tipo/enum/range de chaves conhecidas e descarta chaves desconhecidas (um schema que encolhe nunca serve config stale). A API nunca serve config "crua": a resposta faz merge default_config + overrides do tenant, dropando chaves fora do schema atual.
  • Model: CommunityMicroApp (backend/apps/communities/models/community_micro_app.py, AuditableModel) — FK community (related_name="micro_apps"), app_id CharField, is_enabled Bool, config JSON, order Int; unique constraint (community, app_id); index (community, is_enabled); ordering ["order", "id"]; clean() valida app_id no catálogo + config. Rows são criadas lazy (toggle admin) — uma community sem rows tem tudo desligado.
  • API headless (não é conteúdo de Space → deliberadamente fora do contrato visible/accessible_to do ADR 0010; qualquer membership ativa lê):
    • GET /api/headless/v1/community/micro-apps — só apps is_enabled, com config merged. Staff com ?include_disabled=true recebe o catálogo inteiro com is_enabled (usado pela página de settings).
    • PATCH /api/headless/v1/community/micro-apps/{app_id}admins/moderators apenas (403 para os demais): toggle de is_enabled, order, e merge de config sobre os overrides atuais validado antes de salvar (400 com details se inválido; a row não é criada num PATCH inválido).
    • Orval gera useApiHeadlessV1CommunityMicroAppsList / ...PartialUpdate; contrato OpenAPI em apps/headless/schemas.py (MicroAppSerializer inclui is_enabled opcional — só presente em respostas staff).
  • Frontend: package novo frontend/packages/micro-apps (@repo/micro-apps) — registry.tsx com microAppComponents lazy (lazy() por app), isKnownMicroApp(), getMicroAppComponent(). O metrônomo foi construído do zero (Web Audio lookahead scheduling, useMetronome). Rota /tools/{appId} renderiza o componente registrado com Suspense fallback; seção "Ferramentas" na Sidebar.tsx (mesma query, só apps habilitados); página admin /settings/community/tools (staff) lista o catálogo inteiro com toggles + editor de config do metrônomo.
  • Seed: _seed_micro_apps() em apps/web/management/commands/_seed_gaps/content.py habilita o metrônomo com config default.

Consequences

  • Positive: superfície pequena e previsível — adicionar um micro-app é 1 entry no catálogo + 1 componente lazy; validação, merge e drop de chaves desconhecidas são centralizados no backend, então configs antigas não quebram quando um schema encolhe.
  • Positive: sem runtime de plugins — o que roda é o que está no bundle do frontend, buildado, typechecked e testado como qualquer outro código; nenhuma superfície de execução arbitrária.
  • Positive: API é a mesma fonte de verdade para sidebar e página admin (com include_disabled), seguindo a regra "Orval é a única fonte de verdade para chamadas de API".
  • Negative: um micro-app novo precisa de redeploy de dois lados (catálogo Python + registry frontend) — o custo do design estático; mitigado porque o registry é code-split e o catálogo é um dict.
  • Negative: o editor de config no settings page é específico do metrônomo hoje (a schema de config não é exposta ao frontend). Quando houver mais apps, provavelmente queremos servir o config_schema na API e renderizar editors genéricos — decisão futura, não feita agora.
  • Nota: MicroApp.is_enabled na resposta include_disabled é optional no TS (Orval) — código de render que lê app.is_enabled usa Boolean(...).
  • backend/apps/communities/micro_apps.py — catálogo estático (single source of truth).
  • backend/apps/communities/models/community_micro_app.py — model de enablement/config por tenant; migration 0026_communitymicroapp.
  • backend/apps/headless/views.py::CommunityMicroAppsView/CommunityMicroAppsUpdateView, schemas.py::MicroAppSerializer, urls.py.
  • backend/apps/headless/tests/test_micro_apps.py, backend/apps/communities/tests/test_models.py::CommunityMicroAppTests.
  • backend/apps/web/management/commands/_seed_gaps/content.py::_seed_micro_apps.
  • frontend/packages/micro-apps/src/registry.tsx, metronome/ (manifest, useMetronome, componente), types.ts.
  • frontend/apps/web/src/routes/tools/index.tsx, routes/tools/$appId.tsx, components/Sidebar.tsx (seção "Ferramentas"), routes/settings/community/tools-page.tsx.
  • frontend/packages/api/src/invalidation.ts::useInvalidateMicroAppsQueries.
  • ADR 0010 — micro-apps estão deliberadamente fora do contrato visible/accessible_to (não são conteúdo de Space).
  • ADR 0001 — geração Orval do contrato headless.

Strum — Documentação.