Pular para conteúdo

Implantação — integração Maxsul (PIED)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-08

Como os quatro componentes da integração PIED → X-Adm da Maxsul ficam de pé, como se confere que subiram e como se desligam — hoje em staging. Traz também a config para um dev da XADM rodar os clientes no VSCode.

O desenho — arquitetura, fluxo de processamento, modelo de dados, contratos — é do livro: Documentação Completa, e o porquê do fluxo de entrada está na decisão 0013. Aqui não se repete: o que é daqui é o que subir, com que variáveis, como conferir e como desligar.

Topologia de operação

PIED  ──webhook/REST──▶  int-pied            ──PUT/DELETE /api/v1/xadm──▶  Integrador
                         pied.maxsul.xadm.biz  ◀──GET /api/v1/xadm/retorno──  int.maxsul.xadm.biz
                                                                                    │ grava Postgres (db_maxsul)
                                                                                    ▼
                                                             PowerSync  ◀──replicação lógica── Postgres
                                                             ps.maxsul.xadm.biz
                                                                    │ SQLite streaming
                                                                    ▼
                                              clientes ps-java / ps-dart (réplica no ERP)
                                                    └──write-back status──▶ POST /api/v1/powersync (Integrador)
Componente Host Recurso Coolify Repo
Integrador int.maxsul.xadm.biz App (Dockerfile) integracao/integrador
int-pied pied.maxsul.xadm.biz App (Dockerfile) clientes/maxsul/int-pied
PowerSync ps.maxsul.xadm.biz Docker Compose clientes/maxsul/powersync
Clientes — (rodam no ERP / na máquina do dev) — clientes/maxsul/ps-java, ps-dart

Banco: um Postgres, database db_maxsul (Postgres 18+ — o int-pied usa uuidv7() nativo). Três papéis de conexão:

  • user_maxsul — usuário da aplicação; Integrador e int-pied conectam com ele e escrevem (o int-pied gerencia as tabelas pied_* com histórico Flyway próprio flyway_schema_history_pied; o Integrador é dono das tabelas-espelho e do Flyway principal). Ambos partilham o schema public.
  • powersync_role — só leitura, com privilégio de replicação (WAL) — ver preparação do Postgres.

Convenção: staging agora, produção depois

Hoje estamos em staging: o operador entra no /console do int-pied e libera cada pedido à mão. A promoção para produção é um conjunto de flags a virar — reunidas em Promoção para produção. Todo passo abaixo já traz a coluna staging e a nota do que muda em produção.


1. Integrador — int.maxsul.xadm.biz

Recurso App no Coolify, build do Dockerfile do repo integracao/integrador (Forgejo → Coolify). Detalhe da config: application-config.md e deploy-coolify.md.

Coolify: porta interna 8080 · domínio int.maxsul.xadm.biz (TLS automático) · health check GET /health.

Variáveis de ambiente:

Variável Valor (staging) Nota
CLIENTE maxsul Vira a tag cliente de todo evento GlitchTip (separa o tenant no projeto único).
DATASOURCES_DEFAULT_URL jdbc:postgresql://<host>:5432/db_maxsul JDBC.
DATASOURCES_DEFAULT_USERNAME user_maxsul Usuário-app da Maxsul.
DATASOURCES_DEFAULT_PASSWORD (secret)
INTEGRADOR_API_TOKEN (secret — gerar 1 segredo forte) Bearer estático de /api/**. Mesmo valor usado pelo int-pied e pelos clientes (uploadToken). Nome por destino (quem valida é o integrador), norma da constituição em engenharia/seguranca.md; detalhe local em Segurança. O nome antigo API_BEARER_TOKEN não é lido: app.api-token: ${INTEGRADOR_API_TOKEN:} não tem fallback, e setar só o antigo deixa o validador dormente (/api/** aberto) ou o boot recusado pela guarda da xadm-seguranca.
SENTRY_DSN https://77e5b71d9213456caea902ec36aae742@bug.xadm.biz/16 DSN do projeto GlitchTip integrador (fonte: docs/app.json; igual em todo cliente).
SENTRY_ENVIRONMENT staging Em produção → production.

Sem MICRONAUT_ENVIRONMENTS (produção = application.yml + ENV). PUSH_* ficam desligados (default false) — a Maxsul não usa FCM.

Migrations: o Flyway roda no start; garanta que o user_maxsul tem permissão de DDL no public.


2. int-pied — pied.maxsul.xadm.biz (staging)

Recurso App no Coolify, build do Dockerfile do repo clientes/maxsul/int-pied. Config detalhada: clientes/maxsul/int-pied/docs/operacao/configuracao.md; modo staging: ADR 0004-modo-staging-gate-manual.md do mesmo repo.

Coolify: porta interna 8080 · domínio pied.maxsul.xadm.biz · health GET /health.

Variáveis de ambiente:

Variável Valor (staging) Nota
DATASOURCES_DEFAULT_URL jdbc:postgresql://<host>:5432/db_maxsul Mesmo banco do Integrador (schema public, tabelas pied_*).
DATASOURCES_DEFAULT_USERNAME user_maxsul
DATASOURCES_DEFAULT_PASSWORD (secret)
PIED_TOKEN (secret — token da PIED) Bearer da API PIED. Necessário para o poll e para as ações do /console que buscam na PIED.
PIED_WEBHOOK_HABILITADO true Liga POST /webhook/pied.
PIED_WEBHOOK_SECRET (secret do painel PIED, ou vazio) Vazio = aceita sem validar (MVP). Cadastro do webhook na PIED é manual (ver abaixo).
PIED_POLL_HABILITADO false Staging opera por webhook + console. Ligar (true) só com PIED_TOKEN válido.
PIED_INTEGRACAO_HABILITADA true Liga a Fase 2 (transform + push ao Integrador).
PIED_INTEGRACAO_MODO STAGING Chave do staging: transforma e enfileira (NA_FILA); nada vai ao Integrador até o operador liberar no /console.
INTEGRADOR_BASE_URL https://int.maxsul.xadm.biz Base do Integrador (item 1).
INTEGRADOR_API_TOKEN (o mesmo secret do item 1) Bearer que o int-pied manda ao Integrador (integrador.token). Nome pelo destino, e é o mesmo nome dos dois lados: aqui é para quem eu falo, no item 1 é quem eu valido.
PORT 8080 (default)

Não setar SENTRY_DSN: o entrypoint do container lê docs/app.json (projeto GlitchTip 15) e exporta sozinho. Setar à mão fura a fonte única.

Cadastro do webhook na PIED (manual, uma vez): no painel da PIED, apontar o webhook para https://pied.maxsul.xadm.biz/webhook/pied (com o header X-Pied-Secret = PIED_WEBHOOK_SECRET, se definido).

Segurança: /console, /captura e /test/glitchtip estão abertos, sem auth, e expõem PII e ações ao vivo (push ao X-Adm, fetch na PIED). Proteger (basic-auth do Coolify ou allowlist de IP) antes de expor fora da rede.

Operação em staging (o passo do operador): com pedidos na fila, abrir https://pied.maxsul.xadm.biz/console e usar Processar próximo (libera 1, inspeção item a item) ou Processar todos os pendentes. Cada item liberado vira PUT /api/v1/xadm no Integrador.


3. PowerSync — ps.maxsul.xadm.biz

Recurso Docker Compose no Coolify, repo clientes/maxsul/powersync (imagem journeyapps/powersync-service:1.20.5, config assada na imagem — mudar powersync.yaml = rebuild). Operação do dia a dia: clientes/maxsul/powersync/docs/operacao/runbook.md. Conceitos (volumes, publicação, replica identity): powersync-docker.md e powersync-runbook.md.

3.1. Preparar o Postgres origem (uma vez)

No db_maxsul, habilitar replicação lógica e criar o papel de leitura (detalhe completo e pg_hba.conf em powersync-docker.md):

-- cluster (superusuário), uma vez
ALTER SYSTEM SET wal_level = logical;          -- exige restart do Postgres
CREATE ROLE powersync_role WITH REPLICATION BYPASSRLS LOGIN PASSWORD '<senha_forte>';

-- em db_maxsul
GRANT CONNECT ON DATABASE db_maxsul TO powersync_role;
\c db_maxsul
GRANT USAGE ON SCHEMA public TO powersync_role;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO powersync_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO powersync_role;
CREATE PUBLICATION powersync FOR TABLE contratos, propriedades, fones, itensped, estoque, formulas,
    integracao_remessa, integracao_remessa_item;

As tabelas-espelho nascem na Fase 2 do int-pied e as duas do manifesto de remessa na V27 do Integrador; até existirem, o PowerSync loga "tabela ausente" — não é incidente.

Instalação que já existe (FOR TABLE com lista antiga): o deploy da V27 exige um passo manual. Nenhuma migration toca a publication. Com lista explícita, as tabelas que a V27 cria ficam fora: o PowerSync nunca vê integracao_remessa/_item, o integrador-client recebe manifesto vazio, cai no caminho por presença — e a correção do "filho sem pai" fica inerte sem erro nenhum. Depois da V27 aplicada:

-- confira primeiro: 't' = FOR ALL TABLES (nada a fazer; as tabelas novas entram sozinhas)
SELECT puballtables FROM pg_publication WHERE pubname = 'powersync';
-- se 'f', veja a lista e adicione o que faltar
SELECT tablename FROM pg_publication_tables WHERE pubname = 'powersync';
ALTER PUBLICATION powersync ADD TABLE integracao_remessa, integracao_remessa_item;

(ADD TABLE numa publication FOR ALL TABLES dá erro — por isso a conferência vem antes.) É o mesmo passo que baixa-mdfe-sascar.md já manda para a tabela mdf. Esta lista também estava sem formulas, que produção comprovadamente replica (os logs do incidente de 08/09 mostram o coletor processando formulas) — ou seja, produção não está como este doc dizia; a conferência acima é o que vale, não o doc. O staging/e2e-maxsul-pied (tests) mede isso sozinho: 0-pull-dump -Checar reporta a publication de produção e o 3-verify reprova se o manifesto estiver fora. Tabelas sem PK que entram na publicação precisam de REPLICA IDENTITY (ver powersync-docker.md).

3.2. Variáveis de ambiente (secrets do recurso Coolify)

Variável Valor Nota
PS_DATA_SOURCE_URI postgresql://powersync_role:<senha>@<host>:5432/db_maxsul URI postgresql:// completa (não JDBC); @ na senha → %40.
PS_MONGO_URI mongodb://psadmin:<senha>@mongo-maxsul:27017/powersync_maxsul?authSource=admin&directConnection=true Host = nome do serviço (mongo-maxsul); senha SEM @ nem :.
MONGO_ROOT_USER psadmin Igual ao user embutido em PS_MONGO_URI.
MONGO_ROOT_PASSWORD (secret) Igual à senha embutida em PS_MONGO_URI.
SERVICE_FQDN_POWERSYNC / SERVICE_URL_POWERSYNC ps.maxsul.xadm.biz Magic vars do Coolify → domínio + labels Traefik.

A auth dos clientes (client_auth com JWK inline) já vem no powersync.yaml — ver item 4.4.

3.3. Subir

Pelo Coolify (deploy do recurso) ou, no servidor:

docker compose up -d --build      # config assada na imagem → sempre --build
docker compose logs -f powersync

Nunca docker compose down -v em produção (apaga mongo_storage/mongo_meta → resync total).


4. Clientes ps-java / ps-dart — rodar no VSCode

Não são apps de UI: são CLIs daemon que mantêm a réplica SQLite e fazem write-back. Ambos rodam a partir de dist/ (onde estão a config *.properties e a chave maxsul-private.pem) — rodar de outro diretório quebra o carregamento da chave. Guias no repo: LEIAME.md (operador) e DESENVOLVIMENTO.md (dev).

4.1. ps-java (JDK 21+)

# extensão VSCode: "Extension Pack for Java"; deixar o Gradle sync rodar
./gradlew shadowJar          # gera e copia o jar para dist/
cd dist && ./rodar.sh        # (rodar.bat no Windows)

./gradlew run não funciona out-of-box (cwd errado, não acha a chave). Para debugar no VSCode, criar um launch para a main br.com.xadm.maxsul.ps.Main com working directory = dist/.

4.2. ps-dart (Dart SDK ≥ 3.7, não Flutter)

# extensão VSCode: "Dart"
dart pub get
dart build cli -o dist/build   # NÃO use `dart compile exe` (native assets do PowerSync)
cd dist && ./rodar.sh          # ou, do fonte: cd dist && dart run ../bin/ps_dart.dart

Cópia Windows (C:\Work\Xadm\ps-dart): o bundle fica em dist\bundle\ e roda por dist\rodar.bat. O dist\ps-dart.properties lá vai com os valores descomentados (override ativo) para o ps_dart.exe já compilado funcionar sem recompilar.

4.3. Configuração (ps-java.properties / ps-dart.properties em dist/)

Chave Valor de produção Nota
powersync.syncUrl https://ps.maxsul.xadm.biz PowerSync (item 3).
powersync.uploadUrl https://int.maxsul.xadm.biz/api/v1/powersync Write-back no Integrador.
powersync.uploadToken (o INTEGRADOR_API_TOKEN do item 1) Bearer do POST de upload.
powersync.privateKeyPath maxsul-private.pem Chave que assina o JWT (ver 4.4). Já é default.
jwt.kid / jwt.audience maxsul-ps-v1 / maxsul Batem com o client_auth do PowerSync. Já são default.

4.4. Autenticação dos clientes — JWT self-signed

Os clientes são programas sem usuário nem senha: não "logam", provam posse de uma chave. O cliente assina um JWT curto com a chave privada maxsul-private.pem; o PowerSync valida contra a chave pública correspondente, embutida inline no client_auth do powersync.yaml. Sem serviço de auth, sem login — a chave privada é a credencial (como uma API key / service account). É o padrão máquina-a-máquina, e já vem configurado por default nos dois clientes.

Como está montado (já feito):

  • Par de chaves: maxsul-private.pem (RSA 2048 PKCS#8) entregue no dist/ de cada cliente, fora do git (.gitignore); a pública em clientes/maxsul/powersync/keys/maxsul-public.pem.
  • PowerSync (clientes/maxsul/powersync/powersync.yaml) — client_auth.jwks com a pública inline (kid: maxsul-ps-v1, audience: ['maxsul']). Inline em vez de jwks_uri de propósito: o fetch por URL do auth.xadm.biz deu bug de rede em outras instâncias (vantroba/onpetro).
  • Clientes — defaults já apontam para a chave e os claims certos (powersync.privateKeyPath=maxsul-private.pem, jwt.kid=maxsul-ps-v1, jwt.audience=maxsul, jwt.issuer=maxsul-ps, jwt.subject=maxsul-ps-{java,dart}).

O que o operador/dev precisa fazer: garantir que a maxsul-private.pem está no dist/ do cliente (entregue fora do git) e que o PowerSync foi rebuildado com o powersync.yaml atual (docker compose up -d --build — config assada na imagem). Rodar o cliente → sincroniza sem 401.

Rotação de chave: gerar novo par (openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048), trocar o n e o kid no powersync.yaml, atualizar maxsul-private.pem/jwt.kid no cliente, rebuild do PowerSync. Sintoma de chave/audience errados: 401 PSYNC_S2101 no cliente.

Segurança: quem tiver a maxsul-private.pem emite token válido. Mantê-la fora do git (já no .gitignore), entregar por canal seguro, rotacionar o kid se vazar.


Superfície de operação

O que o operador olha, depois de implantado:

  • https://pied.maxsul.xadm.biz/console — a fila de pedidos: o que foi capturado, o que está NA_FILA, o que foi ENVIADO/confirmado, e as ações (liberar, reenviar, reconciliar).
  • https://pied.maxsul.xadm.biz/console/{code} — detalhe de um pedido, com o de-para dry-run.
  • https://int.maxsul.xadm.biz/ — as telas do integrador (tabelas-espelho, requests, debug).
  • GET /health nos três serviços; logs do PowerSync no Coolify.

Pedido preso em 1XX (erro terminal) — re-armar

1XX é terminal: o integrador-client só coleta linha com cod_retorno em 000 ou 9XX, e o upsert do PUT preserva o retorno por regra (mão única — senão o re-push de rotina apagaria o retorno de pedido já gravado no ERP). Resultado: Reimportar no painel do PIED não destrava um 1XX, por mais vezes que se clique — os pushes chegam e são aplicados, e o cod_retorno continua o mesmo.

O botão Reimportar do painel do int-pied já faz o certo desde 2026-09-08: push do pedido primeiro (dado fresco no espelho), re-armar depois. Use-o. O curl abaixo é o mesmo comando na mão, para diagnóstico ou quando o painel está fora do ar — e sempre depois de o dado corrigido já ter subido, senão o cliente coleta a linha com o valor velho.

curl -fsS -X POST \
  -H "Authorization: Bearer $INTEGRADOR_API_TOKEN" \
  "https://int.maxsul.xadm.biz/api/v1/xadm/retorno/pedido/reenfileirar?chave=260079421"
# → {"xPed":"260079421","contratos":1,"itensped":3,"formulas":2,"total":6}

Volta a 000 todos os itens em 1XX da remessa daquele pedido — as seis tabelas, inclusive o pai (propriedades/fones/estoque). 006 (já no ERP) e 9XX (retenta sozinho) ficam intactas, e linha soft-deletada não entra na conta.

O parâmetro passou a ser chave (o xPed segue valendo como alias), e ele aceita também o cgc_cpf de uma remessa de cadastro de cliente. Consulte antes o que está travado:

curl -fsS -H "Authorization: Bearer $INTEGRADOR_API_TOKEN"   "https://int.maxsul.xadm.biz/api/v1/integracao/remessas?origem=INT-PIED&status=TRAVADA"

A resposta traz, por remessa travada, os itens em 1XX com bloqueado. A causa real é a mensagem do item NÃO bloqueado — os bloqueados são colaterais do pai morto, e reportá-los faz o operador perseguir o sintoma errado. total: 0 significa "não havia nada preso", não falha. Confirmar no ciclo seguinte pelo dXpEnvio novo no integrador-client.log. Contrato completo em Contratos do integrador.

Se total vier certo mas a linha voltar para 1XX no ciclo seguinte, não é o re-armar que falhou: é o write-back do cliente carimbando o mesmo erro de novo — o dado na origem continua errado.

Não é mais fora de escopo: propriedades/fones/estoque presas em 1XX são alcançadas pelo re-arme desde o manifesto de remessa — era o beco que deixava pedido travado sem saída.

Cadastro em 006 com dado corrigido — re-arme manual (caso 260081441)

Desde a revisão 2026-09-17 da decisão 0023, um PUT com dado diferente rearma sozinho a linha 006 de propriedades/fones/estoque. Mas PUT passado não se repete (caminho só de ida): para a linha que já está corrigida no espelho e parada no 006 — como a do cliente abaixo — o re-arme é SQL na mão, depois de alinhar com a equipe X-Adm o que o pAbast faz com o código 0 existente:

UPDATE propriedades
   SET cod_retorno = '000', msg_retorno = NULL, updated_at = now()
 WHERE TRIM(cgc_cpf) = '34586067934' AND TRIM(cod_retorno) = '006';

No ciclo seguinte o client coleta a linha e o dXpEnvio sai com o endereço corrigido. Conferir pelo integrador-client.log (envio N: | ...propriedades ...).

Pedido lançado à mão no X-Adm — fechar a remessa

Há travamento que reenviar não resolve: o produto-kit que o ERP não criou, por exemplo. O operador lança o pedido direto no X-Adm, e a remessa tem de ser fechada, não re-armada. O botão "Importado manualmente" do painel do maxsul-pied faz isso. O curl abaixo é o mesmo comando na mão:

curl -fsS -X POST \
  -H "Authorization: Bearer $INTEGRADOR_API_TOKEN" -H "Content-Type: application/json" \
  -d '{"observacao":"Lançado manualmente no X-Adm: kit sem produto.","operador":"fulano"}' \
  "https://int.maxsul.xadm.biz/api/v1/integracao/remessas/resolver-manual?origem=INT-PIED&chave=260079421"
# → {"chave":"260079421","resolvidas":1}

A remessa vai de TRAVADA a RESOLVIDA_MANUAL, e a observação fica gravada nela. As linhas 1XX não mudam. Só TRAVADA pode ser fechada: ABERTA dá 409, porque ainda está em entrega. Depois disso, o re-arme daquela chave responde 409, porque reenviaria ao ERP o que já foi lançado. Não há comando que desfaça a resolução; se precisar, é SQL na mão. Decisão: 0025.

Verificações pós-deploy

Cada passo declara o resultado esperado conferido no código, não o que parece razoável.

Host Como verificar GlitchTip
int.maxsul.xadm.biz GET /health → {"status":"UP","versao":"X.Y.Z"} (shape do HealthController; o status:"ok" é do /api/health, outro endpoint — não confundir). Provocar um erro controlado (requisição inválida a /api/** que gere log.error) e conferir o evento no projeto integrador filtrando pela tag cliente=maxsul. ✅ projeto 16
pied.maxsul.xadm.biz GET /health → 200. Smoke do GlitchTip: POST https://pied.maxsul.xadm.biz/test/glitchtip → evento no projeto 15. ✅ projeto 15
ps.maxsul.xadm.biz GET /probes/liveness → 200 + docker compose logs -f powersync sem erro de conexão. ⚠️ não reporta — a imagem journeyapps não tem GlitchTip (glitchtip.enabled:false). Verificação é liveness + logs, por decisão.

Fumaça do fluxo de entrada (staging): liberar um pedido no /console → o int-pied dá PUT /api/v1/xadm no integrador → o pedido vira ENVIADO e a linha aparece na tabela-espelho. Repetir o envio do mesmo pedido, sem mudança, → resultado PULADO, não um novo PUT: o PushService compara o content_hash do payload antes de enviar (dirty-check de conteúdo). Um segundo PUT aqui seria sintoma, não normalidade.


Reversão — desligar a integração sem derrubar o X-Adm

O int-pied descobre pedido por duas vias independentes (webhook e poll) e empurra ao integrador por três caminhos: os dois jobs @Scheduled (transform e reconciliação) e o clique do operador no /console. Desligar uma flag não para os envios:

Caminho Efeito
Parar o container do int-pied (Coolify) Para tudo. Mais simples e garantido — prefira este
PIED_INTEGRACAO_HABILITADA=false Para os dois jobs (TransformJob, ReconciliacaoJob). ⚠️ Não para o /console: POST /console/{code}/enviar, /reenviar e /enviar-todos chamam o PushService sem passar pela flag — e o /console está aberto, sem auth. Um clique ainda empurra ao X-Adm
PIED_WEBHOOK_HABILITADO=false ⚠️ Não desliga — corta só a recepção pelo webhook; o que já está capturado segue processável, e o poll (se ligado) segue trazendo pedido novo
PIED_POLL_HABILITADO=false ⚠️ Não desliga — o webhook segue recebendo (é o default true)
PIED_INTEGRACAO_MODO=STAGING ⚠️ Não desliga — só troca envio automático por manual; o operador segue empurrando pelo /console
Parar o container do integrador Para a entrada no X-Adm, mas o int-pied segue capturando e acumulando falha de push (ERRO)

Desligar de verdade, mantendo as telas no ar: PIED_INTEGRACAO_HABILITADA=false e bloquear o /console (auth/allowlist no Coolify). Sem as duas, a via manual continua aberta.

Em qualquer caso a PIED segue postando no webhook (ou o poll acumula), e o atraso sai quando religar — nada se perde.

Reverter versão: redeploy da tag anterior no Coolify. As migrations do int-pied (pied_*, com histórico Flyway próprio) e as do integrador são aditivas — reverter o container não desfaz schema.


Promoção para produção

Quando sair de staging, virar (na ordem):

  1. int-pied — PIED_INTEGRACAO_MODO=PRODUCAO (push automático, sem gate no /console); PIED_POLL_HABILITADO=true (com PIED_TOKEN válido) se quiser o backstop REST; confirmar PIED_WEBHOOK_SECRET real com a PIED.
  2. int-pied — proteger /console, /captura, /test/glitchtip (auth/allowlist). Não é só PII: enquanto o /console está aberto, ele é uma via de push ao X-Adm que nenhuma flag corta (§ Reversão).
  3. Integrador — SENTRY_ENVIRONMENT=production.
  4. Clientes — maxsul-private.pem de produção entregue no dist/ e PowerSync rebuildado com o JWK inline (item 4.4). Trocar a chave dev-only por uma chave de produção, se ainda não feito.

Pares que têm de casar (segredos compartilhados — não commitar)

Valor que existe nos dois lados e diverge em silêncio é falha de implantação que só aparece depois, em produção. Enumerados:

Ponta A Ponta B Sintoma se divergirem
INTEGRADOR_API_TOKEN (integrador, define) INTEGRADOR_API_TOKEN (int-pied) PUT /api/v1/xadm responde 401; o pedido fica ERRO no /console, nada entra no espelho
INTEGRADOR_API_TOKEN (integrador) powersync.uploadToken (ps-java / ps-dart) write-back do status responde 401; o ERP replica mas nunca confirma — o pedido trava em ENVIADO
Senha do powersync_role (Postgres) PS_DATA_SOURCE_URI PowerSync não conecta na origem; sem replicação, cliente não recebe nada
MONGO_ROOT_PASSWORD senha embutida em PS_MONGO_URI PowerSync sobe e falha no storage; /probes/liveness denuncia
maxsul-private.pem (dist/ dos clientes) JWK público inline no powersync.yaml 401 PSYNC_S2101 no cliente (ver 4.4)
PIED_WEBHOOK_SECRET (int-pied) header X-Pied-Secret cadastrado no painel PIED webhook rejeitado — a PIED para de entregar e a captura fica só no poll (se ligado)

A chave privada maxsul-private.pem fica fora do git (.gitignore) e é entregue por canal seguro; quem a tiver emite token válido.

Referências