Uma página simples pra receber apoio via Pix — pra quem mantém projeto open source no Brasil. Inspirada no Buy Me a Coffee, mas feita pro público daqui: só Pix, sem pedir e-mail, CPF ou telefone.
Self-hosted: roda no seu servidor, com a sua conta Pix.
Nome e apoiadores de demonstração, com dados fictícios.
- Uma página, com timeline pública de quem apoiou, quanto e a mensagem que deixou.
- Apoiar não exige cadastro: valor, nome e mensagem (os dois opcionais), Pix. Acabou.
- Anônimo de verdade: quem não quer aparecer vira "Anônimo" + valor na vitrine. Nome e mensagem continuam no seu banco, visíveis só pra você.
- Avatares gerados (DiceBear) a partir de um ID opaco — nunca do nome ou do e-mail de ninguém.
- Uma página por projeto:
/financeiromostra "Apoie Felipe no desenvolvimento do Financeiro"; a raiz aceita apoio genérico. Tudo cai na mesma timeline. - Link bonito ao compartilhar: imagem de Open Graph gerada na hora, com o seu avatar e o seu nome.
- Um admin pra configurar tudo — identidade, projetos, valores sugeridos — e pra ver os apoios com nome e mensagem reais.
docker run -d \
--name apoia \
--restart unless-stopped \
-p 3000:3000 \
-v apoia_data:/data \
-e APOIA_SITE_URL="https://seu-dominio.com" \
-e PIX_PROVIDER="woovi" \
-e WOOVI_APP_ID="..." \
-e APOIA_ADMIN_EMAIL="voce@gmail.com" \
-e APOIA_ADMIN_SECRET="cole-aqui-o-segredo-gerado" \
ghcr.io/valtlfelipe/apoia:latestAPOIA_SITE_URL, WOOVI_APP_ID, APOIA_ADMIN_EMAIL e APOIA_ADMIN_SECRET são o
mínimo obrigatório. PIX_PROVIDER já tem woovi como padrão e está aí só pra
deixar visível que o provedor de Pix é plugável — veja
Adicionando outro provedor. As demais
opcionais estão em Configuração; se forem muitas, --env-file .env
no lugar dos vários -e.
Gere o segredo uma vez com openssl rand -base64 32 e guarde: ele assina o
cookie de sessão, então trocar depois derruba o seu login no admin.
Depois acesse https://seu-dominio.com/admin e entre com a conta Google de
APOIA_ADMIN_EMAIL — é lá que você configura nome, avatar, links e projetos. Até o
primeiro login a instância roda com os padrões do código (nome "Apoia", sem
projetos).
Falta ainda registrar o webhook da Woovi, senão o pagamento nunca confirma.
Pra atualizar, puxe a imagem nova e recrie o container — os dados ficam no
volume apoia_data e sobrevivem:
docker pull ghcr.io/valtlfelipe/apoia:latest
docker rm -f apoia
# rode o mesmo `docker run` de novoSe a sua plataforma pede um healthcheck (Railway, um HEALTHCHECK de Docker, um
load balancer), aponte pra /api/health — ele responde 200 e toca o banco, o
que também pega o caso do volume que não montou.
As migrations do banco rodam sozinhas a cada boot, antes do servidor subir. Tags
publicadas a cada release: latest, X.Y.Z, X.Y e X; edge sai quando o
workflow é disparado manualmente a partir da main. Rodando um fork? A sua imagem
vai pra ghcr.io/<seu-usuário>/<seu-fork>.
Obrigatórias:
| Variável | Descrição |
|---|---|
APOIA_SITE_URL |
URL pública desta instância. Usada nos metadados, no registro do webhook e como origin do login. |
APOIA_ADMIN_EMAIL |
A única conta Google que entra no /admin. |
APOIA_ADMIN_SECRET |
Assina o cookie de sessão do admin. Gere com openssl rand -base64 32. |
Mais as do provedor de Pix escolhido em PIX_PROVIDER — só as dele, o outro
bloco pode ficar vazio. Nenhuma delas vai para o navegador.
| Provedor | Variável | Descrição |
|---|---|---|
woovi |
WOOVI_APP_ID |
App ID do painel da Woovi → Applications. |
abacatepay |
ABACATEPAY_API_KEY |
Chave do painel da AbacatePay → API keys. |
abacatepay |
ABACATEPAY_WEBHOOK_SECRET |
Secret que autentica o webhook. Gere com openssl rand -base64 32. |
Opcionais:
| Variável | Padrão | Descrição |
|---|---|---|
APOIA_RATE_LIMIT_PER_MINUTE |
5 |
Cobranças criadas por IP, por minuto. |
DATABASE_PATH |
/data/apoia.db no Docker |
Arquivo SQLite. |
PIX_PROVIDER |
woovi |
Qual módulo de lib/pix/providers usar: woovi ou abacatepay. |
WOOVI_API_URL |
https://api.woovi.com/api/v1 |
Sandbox: api.woovi-sandbox.com. |
WOOVI_WEBHOOK_TOKEN |
— | Token extra conferido no header Authorization do webhook. |
ABACATEPAY_API_URL |
https://api.abacatepay.com/v2 |
O sandbox é decidido pela chave (devMode), não pela URL. |
ABACATEPAY_WEBHOOK_PUBLIC_KEY |
— | Sobrescreve a chave HMAC publicada pela AbacatePay (rotação de chave). |
A chave pública que valida a assinatura do webhook da Woovi não aparece aí
porque não é configurável: ela é buscada em WOOVI_API_URL/webhook/public-keys
e mantida em cache por uma hora, então uma rotação de chave na Woovi é
acompanhada sozinha. A AbacatePay não publica um endpoint equivalente — a chave
dela é fixa e a mesma para todo mundo — daí a variável acima.
Lista completa e comentada em .env.example. Todo o resto — nome,
avatar, links, projetos, valores sugeridos, mensagem de agradecimento — se
configura no /admin, não por variável de ambiente.
-
Crie uma conta em woovi.com e gere um App ID em Applications.
-
Passe em
WOOVI_APP_ID. -
Com a instância no ar numa URL HTTPS pública, registre o webhook apontando pra
$APOIA_SITE_URL/api/webhooks/woovi:export WOOVI_APP_ID="..." # o mesmo do deploy export WOOVI_API_URL="https://api.woovi.com/api/v1" # sandbox: api.woovi-sandbox.com export APOIA_SITE_URL="https://seu-dominio.com" for event in OPENPIX:CHARGE_COMPLETED OPENPIX:CHARGE_EXPIRED; do curl -X POST "$WOOVI_API_URL/webhook" \ -H "Authorization: $WOOVI_APP_ID" \ -H "Content-Type: application/json" \ -d "{\"name\":\"apoia — $event\",\"event\":\"$event\",\"url\":\"$APOIA_SITE_URL/api/webhooks/woovi\",\"isActive\":true}" done
Passo único por deploy — refaça só se
APOIA_SITE_URLmudar. Dá pra fazer pelo painel da Woovi também; não existe script deste projeto que faça isso por você.
-
Crie uma conta em abacatepay.com e gere uma API key em API keys. Uma chave de devMode manda tudo pro sandbox — a URL é a mesma.
-
Passe em
ABACATEPAY_API_KEY, e gere umABACATEPAY_WEBHOOK_SECRETcomopenssl rand -base64 32. -
Com a instância no ar numa URL HTTPS pública, registre o webhook apontando pra
$APOIA_SITE_URL/api/webhooks/abacatepay:export ABACATEPAY_API_KEY="..." # a mesma do deploy export ABACATEPAY_WEBHOOK_SECRET="..." # o mesmo do deploy export APOIA_SITE_URL="https://seu-dominio.com" curl -X POST "https://api.abacatepay.com/v2/webhooks/create" \ -H "Authorization: Bearer $ABACATEPAY_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"name\":\"apoia\",\"endpoint\":\"$APOIA_SITE_URL/api/webhooks/abacatepay\",\"secret\":\"$ABACATEPAY_WEBHOOK_SECRET\",\"events\":[\"transparent.completed\"]}"
Passo único por deploy — refaça só se
APOIA_SITE_URLmudar. Dá pra fazer pelo painel da AbacatePay também.
Pra testar sem dar um Pix de verdade, com uma chave de devMode, pegue o id da
cobrança (pix_char_..., na coluna Referência do /admin/supports) e simule o
pagamento:
curl -X POST "https://api.abacatepay.com/v2/transparents/simulate-payment?id=pix_char_..." \
-H "Authorization: Bearer $ABACATEPAY_API_KEY"Duas diferenças em relação à Woovi, ambas por limitação da API deles: não existe
evento de expiração (uma cobrança que vence é pega pelo polling), e não há
end-to-end id em pagamentos recebidos — o /admin/supports mostra o id da cobrança
no lugar.
O webhook é a fonte de verdade da confirmação. A página também faz polling de status como reforço, útil onde o webhook ainda não está configurado (dev local).
Sem APOIA_ADMIN_EMAIL/APOIA_ADMIN_SECRET a instância nem sobe — é por ali que
se configura o site:
- Configurações: identidade do criador (nome, nome curto usado nas headlines, tagline, avatar, links) e o formulário de apoio (valores sugeridos, mínimo e máximo, o que a timeline mostra, estilo do avatar, validade da cobrança, mensagem de agradecimento).
- Produtos: criar, editar, ativar/desativar e — se ainda não tiver apoios —
excluir as páginas
/<slug>. - Apoios: a lista com nome e mensagem reais, mesmo de quem escolheu aparecer como anônimo, com um botão pra ocultar (ou devolver) alguém da vitrine. Veja Privacidade e segurança.
- Sobre: versão instalada, checagem de nova release e links pro repositório.
O login usa o Google via shoo.dev, um broker de autenticação
minimalista: ele confirma sua identidade e devolve um token assinado, que o apoia
verifica no servidor e só aceita se o e-mail bater com APOIA_ADMIN_EMAIL. A
sessão é um cookie próprio do apoia, independente do shoo.
- Nenhum dado pessoal é coletado além do que a pessoa opcionalmente digita: nome e mensagem. Sem e-mail, CPF ou telefone.
- Anônimo na vitrine, não no banco: com "aparecer na timeline" desmarcado, nem a
página nem a API pública devolvem o nome ou a mensagem reais — só "Anônimo" e o
valor. O dado real fica no SQLite, visível só pelo
/admin. - Payload do webhook é higienizado antes de salvar: a Woovi manda nome e CPF de quem pagou no evento de confirmação; esse bloco é removido antes de qualquer persistência, inclusive do log de auditoria.
- Assinatura do webhook é verificada (RSA, chave pública da Woovi) antes de processar qualquer payload.
- Rate limiting por IP na criação de cobranças — o IP nunca é salvo, só um hash em memória que expira.
- Headers de segurança (CSP, HSTS,
X-Frame-Options) por padrão emnext.config.ts.img-srcaceita qualquer origemhttps:, porque a URL do avatar é configurável em runtime;script-srccontinua travado em'self'.
Pra mexer no código — não é necessário só pra hospedar. Requer Node.js 24+ e pnpm.
git clone https://github.com/valtlfelipe/apoia.git
cd apoia
pnpm install
cp .env.example .env # edite com seus dados
pnpm db:generate # só na primeira vez, ou após mudar lib/db/schema.ts
pnpm db:migrate
pnpm devSem um WOOVI_APP_ID de verdade a criação de cobrança falha (esperado), mas página,
timeline e validações funcionam normalmente com linhas inseridas direto no SQLite.
Testar a confirmação de ponta a ponta exige uma conta Woovi (sandbox serve) — sem
ela o webhook nunca chega, e a assinatura precisa bater com o payload real.
Pra testar mudanças no Dockerfile, o docker-compose.yml do repo builda local:
docker compose up -d --build.
| Comando | O que faz |
|---|---|
pnpm dev |
Servidor de desenvolvimento. |
pnpm build / pnpm start |
Build e start de produção. |
pnpm db:generate |
Gera uma migration a partir de lib/db/schema.ts. |
pnpm db:migrate |
Aplica as migrations pendentes. |
pnpm db:studio |
Abre o Drizzle Studio. |
pnpm check |
Lint (Biome) + typecheck. |
Next.js 16 (App Router) · React 19 · Tailwind CSS v4 · Drizzle ORM · SQLite
(better-sqlite3) · Zod · TypeScript.
O driver do SQLite fica isolado em lib/db/client.ts — é o único arquivo que fala
com ele direto, então trocar por node:sqlite (nativo, ainda experimental) é mexer
só ali.
Toda a integração passa pela interface PixProvider (lib/pix/types.ts); nenhum
componente de UI ou lógica de domínio referencia um provedor específico — a Woovi e
a AbacatePay são os dois que já existem, e servem de referência pros dois formatos
possíveis de webhook (assinatura RSA no header vs. secret na query). Pra adicionar
um:
- Crie
lib/pix/providers/<nome>.tsimplementandoPixProvider(createCharge,getChargeStatus,verifyWebhook,parseWebhook,redactWebhookPayload). - Registre em
lib/pix/index.ts. - Adicione o nome ao enum
PIX_PROVIDERemlib/config/env.ts.
Pra quem mantém o projeto:
- Mova as mudanças de
[Unreleased]pra uma nova seção noCHANGELOG.md, no formato## [X.Y.Z] - AAAA-MM-DD. - Atualize
"version"nopackage.jsonpro mesmo número. git tag vX.Y.Z && git push origin main vX.Y.Z
O workflow builda a imagem multi-arch (linux/amd64 e linux/arm64), publica no
GHCR e cria a GitHub Release com as notas tiradas da seção correspondente do
changelog.
AGPL-3.0-only.
