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:
- 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).
- 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.
- Catálogo estático + enablement por tenant + componentes lazy no frontend — escolhida. 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ó registrais_enabled,config(JSON de overrides) eorder. Adicionar um micro-app = uma entry no catálogo + um componente lazy no registry do frontend, ambos chaveados pelo mesmoapp_id. Nada de carregamento dinâmico de plugin, nada de DB definindo comportamento.
Decisão
- Catálogo estático:
backend/apps/communities/micro_apps.py—MICRO_APPS: dict[str, MicroAppDefinition], cada definição comapp_id,name,description,category,config_schema(shape JSON-schema minimal) edefault_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 mergedefault_config+ overrides do tenant, dropando chaves fora do schema atual. - Model:
CommunityMicroApp(backend/apps/communities/models/community_micro_app.py,AuditableModel) — FKcommunity(related_name="micro_apps"),app_idCharField,is_enabledBool,configJSON,orderInt; unique constraint (community,app_id); index (community,is_enabled); ordering["order", "id"];clean()validaapp_idno 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ó appsis_enabled, com config merged. Staff com?include_disabled=truerecebe o catálogo inteiro comis_enabled(usado pela página de settings).PATCH /api/headless/v1/community/micro-apps/{app_id}— admins/moderators apenas (403 para os demais): toggle deis_enabled,order, e merge deconfigsobre os overrides atuais validado antes de salvar (400 comdetailsse inválido; a row não é criada num PATCH inválido).- Orval gera
useApiHeadlessV1CommunityMicroAppsList/...PartialUpdate; contrato OpenAPI emapps/headless/schemas.py(MicroAppSerializerincluiis_enabledopcional — só presente em respostas staff).
- Frontend: package novo
frontend/packages/micro-apps(@repo/micro-apps) —registry.tsxcommicroAppComponentslazy (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" naSidebar.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()emapps/web/management/commands/_seed_gaps/content.pyhabilita 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_schemana API e renderizar editors genéricos — decisão futura, não feita agora. - Nota:
MicroApp.is_enabledna respostainclude_disabledéoptionalno TS (Orval) — código de render que lêapp.is_enabledusaBoolean(...).
Related
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; migration0026_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.