Construir um app no marketplace
Apps no marketplace do MIX são pacotes TypeScript que rodam no nosso backend. Você define um manifest, expõe actions tipadas em Zod e triggers de eventos do provider. O MIX cuida de OAuth, refresh, armazenamento de credenciais e UI de configuração.
Anatomia de um app
Todo app exporta um único objeto via registerProvider(). O contrato é o seguinte:
import { z } from "zod";
import { registerProvider } from "@/marketplace/registry";
export default registerProvider({
manifest: {
slug: "minha-integracao",
name: "Minha Integração",
category: "communication",
iconUrl: "https://exemplo.com/icon.svg",
tagline: "Conecta o MIX ao serviço X.",
description:
"Descrição longa em prosa. Aparece na página do app.",
auth: { kind: "oauth2" }, // ou "api_key" | "none"
scopesRequested: ["read", "write"],
website: "https://exemplo.com",
},
oauth: {
authorizationUrl: "https://provider.com/oauth/authorize",
tokenUrl: "https://provider.com/oauth/token",
scopeJoin: " ", // como concatenar scopes
},
actions: {
sendNotification: {
label: "Enviar notificação",
description: "Manda push para o usuário.",
input: z.object({
userId: z.string(),
message: z.string().min(1).max(200),
}),
handler: async ({ input, credentials }) => {
const res = await fetch("https://provider.com/api/v1/notify", {
method: "POST",
headers: { Authorization: `Bearer ${credentials.accessToken}` },
body: JSON.stringify(input),
});
if (!res.ok) throw new Error(`provider error: ${res.status}`);
return { ok: true };
},
},
},
triggers: {
notificationDelivered: {
label: "Notificação entregue",
description: "Dispara quando o usuário recebe a mensagem.",
output: z.object({
userId: z.string(),
deliveredAt: z.string().datetime(),
}),
},
},
webhook: {
path: "events",
verify: async ({ rawBody, headers, credentials }) => {
// Implemente verificação HMAC do provider.
return true;
},
handle: async ({ event, emit }) => {
if (event.type === "notification.delivered") {
emit("notificationDelivered", {
userId: event.userId,
deliveredAt: event.timestamp,
});
}
},
},
onInstall: async ({ tenantId, credentials }) => {
// Provisionar recursos no provider, se necessário.
},
onUninstall: async ({ tenantId, credentials }) => {
// Limpar resources externos.
},
});Autenticação
Três modos suportados em manifest.auth.kind:
oauth2— fluxo authorization_code padrão. Você forneceauthorizationUrletokenUrl; o MIX orquestra o resto. Suporta PKCE quando o provider exige.api_key— usuário cola uma key no app. Ela vai criptografada pracredentials.apiKeynas suas actions.none— apps que não precisam autenticar (ex.: módulos internos do MIX, webhooks públicos sem segredo).
Fluxo OAuth detalhado
// 1. MIX redireciona usuário para provider.com/oauth/authorize
// com ?client_id=...&redirect_uri=...&state=...
// 2. Após autorização, provider redireciona pra:
// https://api.usemix.app/marketplace-callback?code=...&state=...
// 3. MIX faz POST pro tokenUrl trocando code por accessToken:
const res = await fetch("https://provider.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri,
client_id,
client_secret,
}),
});
const { access_token, refresh_token, expires_in } = await res.json();
// 4. MIX armazena criptografado em `app_installations.credentials`
// com tenant secret. Refresh é automático antes de expirar.Actions tipadas
Actions são funções que o MIX (ou automações do usuário) chama para executar algo no provider. Inputs são validados via Zod — schemas ruins falham antes de chegar no handler.
labeledescriptionaparecem na UI de configuração de automação.inputé um schema Zod. Use.optional()pros campos opcionais.handlerrecebe{ input, credentials, tenantId, logger }e retorna o resultado.- Erros lançados são logados em
app_action_runse aparecem no audit log do tenant.
Triggers (eventos do provider)
Triggers expõem eventos do provider como gatilhos pra automações no MIX. Você declara o nome e o schema do payload — quando o webhook chega, você chama emit() dentro do webhook.handle com o nome do trigger e os dados.
Usuários do MIX então criam automações tipo "Quando trigger X, cria deal" sem precisar saber nada do payload bruto.
Webhooks (recepção do provider)
Se o seu app expõe triggers, é provável que precise receber webhooks do provider. O MIX já provê uma URL única por instalação:
https://api.usemix.app/marketplace-webhooks/<app-slug>/<installation-id>Quando esse endpoint recebe POST, o MIX chama seu webhook.verify() e em sucesso chama webhook.handle(). Sempre verifique a assinatura — receber payload sem verificar é a mesma falha de segurança dos webhooks de saída.
verify: async ({ rawBody, headers, credentials }) => {
const sig = headers["x-provider-signature"];
const expected = crypto
.createHmac("sha256", credentials.webhookSecret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(sig, "hex"),
Buffer.from(expected, "hex"),
);
}Ciclo de vida
onInstall— chamado quando um tenant ativa o app. Recebe credenciais. Use pra registrar webhooks no provider, criar recursos remotos, validar permissões.onUninstall— quando o tenant desconecta. Use pra limpar webhooks/recursos remotos. Sempre seja idempotente — pode ser chamado mais de uma vez.
Testar localmente
- Solicite o repositório template e os exemplos atualizados em contato com o time de partners (ver final desta página).
- Solicite um workspace de desenvolvimento via suporte no app (não use produção pra dev).
- Use ngrok / Cloudflare Tunnel pra expor seu provider mockado.
- Rode o sandbox:
pnpm marketplace:sandbox. Ele monta seu manifest contra o tenant de dev e simula chamadas.
Publicação
- App passa por code review interno.
- Você assina o App Partner Agreement (cobertura de incidentes, LGPD, suporte).
- App vai pra preview — usuários veem com badge "Beta" e telemetria de uso é enviada pra você semanalmente.
- Após 30 dias sem incidentes, app sai de preview e fica público.