diff --git a/docs/changelog/2026-08-24-s11-polish.md b/docs/changelog/2026-08-24-s11-polish.md
new file mode 100644
index 00000000..95bba42c
--- /dev/null
+++ b/docs/changelog/2026-08-24-s11-polish.md
@@ -0,0 +1 @@
+**S11-POLISH (#761) — rota de sync: 400 em não-configurado e tipo de persist deduplicado:** `POST /api/social-feed/sync` agora devolve `400 { ok:false, error }` quando o Instagram não está configurado (antes `200 { ok:false, status:{} }`), alinhando código ao impl doc da S11 (`400` previsto) — o painel `InstagramSyncStatusPanel` já tratava `!response.ok` e nem renderiza o botão nesse estado, então é correção de contrato HTTP observável só via curl/test direto (sem `revalidateTag` no branch 400). `InstagramPersistDatabase` (`instagramFeed.ts:335`) deduplicado: virou alias `export type InstagramPersistDatabase = PostgresTransactionDatabase` (`postgresTransactionLocks.ts:6`, owner único, `import type` sem custo runtime, `@deprecated` — preferir o owner), `instagramSync.ts` passa a importar `PostgresTransactionDatabase` direto. Débitos 3 (`jsonb` last-writer-wins, janela 300s auto-curável) e 4 (`admin.e2e` guard `· 5 posts`) permanecem defer/descartar com gatilho documentado no impl plan.
diff --git a/docs/plans/s11-polish-rota-de-sync-impl.md b/docs/plans/s11-polish-rota-de-sync-impl.md
new file mode 100644
index 00000000..5a0d162c
--- /dev/null
+++ b/docs/plans/s11-polish-rota-de-sync-impl.md
@@ -0,0 +1,85 @@
+# Impl: S11-POLISH — rota de sync: 200 ok:false em não-configurado e tipo de persist duplicado
+
+Status: aprovado
+Atualizado em: 2026-08-24
+Issue: #761
+Intenção: body da Issue #761 (débito P3 do /simplify da S11, sem plano externo)
+Appetite restante: ~0.5 dia (polish P3, sem migration)
+
+## Leitura da intenção
+
+- **Outcome:** alinhar o contrato da rota `POST /api/social-feed/sync` com o impl doc da S11 e eliminar a duplicação de tipo `InstagramPersistDatabase` ↔ `PostgresTransactionDatabase` sem mudar comportamento observável da home, da global ou do painel.
+- **O que NÃO negociar:** home permanece fail-closed (YouTube+artigos quando IG falha); token nunca fora do admin; sem wizard OAuth; painel só mostra estado persistido (5 estados do rascunho); sem histórico/alertas. Achados 3 (`jsonb` last-writer-wins) e 4 (`admin.e2e` guard `· 5 posts`) ficam registrados e fora deste escopo — o issue body já os marca como débito absorvido/defer.
+- **O que reavaliar:** hipótese do débito 1 de que o desacoplamento via tipo duplicado evita dependência do módulo de locks — na prática `instagramSync.ts` já importa `getPostgresTransactionDatabase`; tipo é `type-only`, sem custo de runtime. Hipótese do débito 2 de que `200 { ok:false, status:{} }` é defensivo porque o botão não renderiza quando `!configured` — verdade para UI, mas o contrato HTTP deve ser 400 para caller direto (curl/test) como o impl doc previa.
+
+## Abordagem recomendada
+
+```mermaid
+flowchart LR
+ A[POST /api/social-feed/sync] --> B{isInstagramFeedConfigured?}
+ B -->|não| C[400 { ok:false, error }]
+ B -->|sim| D[syncInstagramFeed]
+ D --> E[200 { ok, status }]
+ F[instagramFeed.ts
InstagramPersistDatabase] -.-> G[postgresTransactionLocks.ts
PostgresTransactionDatabase
owner]
+ G --> F
+```
+
+**Opções consideradas:** A | B | C
+**Recomendação:** A — rota devolve 400 com `error` quando IG não configurado; tipo de persist deduplicado via alias `InstagramPersistDatabase = PostgresTransactionDatabase` cujo owner é `postgresTransactionLocks.ts`.
+**Rejeitadas:** B) manter `200` e atualizar impl doc para documentar `200` (esconde erro de cliente como sucesso; quebra semântica REST e diverge de todo outro guard da rota que usa 4xx) | C) extrair interface genérica `DatabaseWithExecute` em `utilities/db.ts` (1 nível a mais para 2 call sites; viola depth check — o owner já existe e é `postgresTransactionLocks.ts`).
+
+### Decisões de engenharia
+
+1. **Status HTTP para não-configurado: 400 em vez de 200.**
+ Opções: A) rota retorna `400 { ok:false, error:'Instagram não configurado' }` quando `syncInstagramFeed` indica não-configurado | B) manter `200 { ok:false, status:{} }` e corrigir impl doc | C) rota chama `isInstagramFeedConfigured` antes do sync e retorna 400 sem chamar `syncInstagramFeed`.
+ Recomendação: A com variante leve de C — a rota checa `isInstagramFeedConfigured` via `syncInstagramFeed` já fazer o check: o mais barato é fazer a rota inspecionar o outcome: se `!ok && status vazio` → 400. Alternativa ainda mais explícita: a rota re-lê a global e checa `isInstagramFeedConfigured` antes de chamar `sync` (1 read extra mas sem custo de API) e retorna 400. Escolher a variante sem read extra (outcome vazio → 400) para não adicionar query.
+ Rejeitadas: B porque 200 mente sobre sucesso e o painel já trata `!response.ok` como erro (linha 101 do `InstagramSyncStatusPanel.tsx`: `if (!response.ok || body.error) throw`) — 400 cai no caminho de erro existente sem mudar UI; C adiciona round-trip desnecessário quando o sync já sabe que não está configurado.
+ → Contrato: `POST /api/social-feed/sync` → não-configurado: `400 { ok:false, error:'Instagram não configurado — informe token e ID da conta.' }`; configurado: `200 { ok, status }` (sucesso ou falha da Graph API). `revalidateTag` só roda no caminho 200.
+
+2. **Deduplicação de tipo `InstagramPersistDatabase`.**
+ Opções: A) `instagramFeed.ts` importa `PostgresTransactionDatabase` e re-exporta `export type InstagramPersistDatabase = PostgresTransactionDatabase` (alias compatível) | B) remover `InstagramPersistDatabase` e migrar todos os consumidores para `PostgresTransactionDatabase` | C) manter duplicação.
+ Recomendação: A — owner único (`postgresTransactionLocks.ts`), `instagramFeed.ts` vira alias type-only (sem duplicar shape `{ execute }`), `instagramSync.ts` passa a importar `PostgresTransactionDatabase` diretamente para `resolvePersistDatabase`. Mantém compatibilidade de import path para quem ainda importa `InstagramPersistDatabase` de `instagramFeed.ts` (nenhum teste externo hoje, mas sem churn).
+ Rejeitadas: B exige mudar assinatura pública de `persistInstagramSnapshot`/`persistInstagramSyncStatus`/`persistInstagramAccessToken` em 3 lugares só por rename — churn sem ganho; C mantém débito técnico exatamente como reportado (2 definições estruturais idênticas).
+ → Futura remoção do alias pode ser feita quando todos os call sites migrarem, sem pressa.
+
+### Componentes / mudanças
+
+- **`src/utilities/socialFeed/instagramFeed.ts`** (editar): importar `PostgresTransactionDatabase` de `@/utilities/postgresTransactionLocks`; substituir definição estrutural de `InstagramPersistDatabase` por `export type InstagramPersistDatabase = PostgresTransactionDatabase` (alias + comentário apontando owner); assinaturas de `persistInstagramSyncStatus`/`persistInstagramSnapshot`/`persistInstagramAccessToken` passam a usar `PostgresTransactionDatabase | null` (via alias mantém compat).
+- **`src/utilities/socialFeed/instagramSync.ts`** (editar): importar `PostgresTransactionDatabase` (ou manter alias via `instagramFeed.ts`); `resolvePersistDatabase` retorna `Promise`; sem mudança de lógica.
+- **`src/app/(frontend)/api/social-feed/sync/route.ts`** (editar): após `syncInstagramFeed`, se `!outcome.ok && Object.keys(outcome.status).length === 0` → `400 { ok:false, error:'Instagram não configurado — informe token e ID da conta.' }` (sem `revalidateTag`); senão `200 { ok, status }` + `revalidateTag`. Mantém 401/403/500 existentes.
+- **`tests/unit/instagramSync.unit.spec.ts`** (editar/estender): novo caso "rota retorna 400 quando não configurado" não cabe em unit sync — cobrir em teste de rota leve ou em unit da rota se existir; no sync unit, garantir que `outcome` vazio ainda é `{}` para o gate da rota funcionar. Opcional: teste de tipo (tsc) confirma alias.
+- **`docs/plans/feed-instagram-nao-aparece-home-impl.md`** (sem alteração necessária): já previa `400 { ok:false, error }` — após fix o código alinha, doc fica correto.
+- **Migration:** sem migration (sem coluna nova).
+- **Access / Consent:** sem mudança (rota já exige `users` + `isSameOriginRequest`; sem PII novo).
+- **UI:** sem mudança visual no painel; `InstagramSyncStatusPanel.tsx` já trata `!response.ok` como erro — 400 cai no `throw` → `requestError` genérico, mas botão nem renderiza quando `!configured` (early return), então 400 só é observável via curl/test direto, como pretendido.
+
+### Dados → forma (se aplicável)
+
+Não aplicável — sem KPI/métrica nova. Status continua shape `{ lastSyncAt?, postCount?, error?, errorAt? }` jsonb.
+
+## Fases verificáveis
+
+1. **Tipo + rota (tracer)** — editar `instagramFeed.ts` alias + `instagramSync.ts` import + `route.ts` branch 400; `pnpm tsc --noEmit`; `pnpm test` (unit `instagramSync`); curl manual: `POST /api/social-feed/sync` sem credenciais → 400 com `error`, com credenciais stub ok → 200.
+2. **Gates** — `pnpm gate:fast` (lint+type+unit); `pnpm test` full local; `pnpm build` local (importmap); sem e2e novo obrigatório — e2e existente de sync deve continuar verde (retry quando configured).
+
+## Rabbit holes / Não escopo (engenharia)
+
+- Não versionar `instagramSyncStatus` com seq/timestamp para mitigar last-writer-wins (achado 3) — defer com gatilho: se regressão for observada em item futuro, versionar.
+- Não mudar `admin.e2e.spec.ts` assert `· 5 posts` (achado 4) — guard intencional.
+- Não extrair módulo genérico de DB nem mover `resolvePersistDatabase` para lib compartilhada.
+- Não paginar feed nem tocar no refresh-token flow.
+- Não adicionar histórico de sync nem dashboard.
+
+## Riscos e mitigação
+
+- **Rota 400 quebra painel que espera 200:** mitigado — painel já confere `!response.ok` (linha 101) e mostra `requestError`; e o botão não existe quando `!configured`, então o 400 nunca é exercitado pela UI normal. Teste manual do painel em 3 estados (não configurado/desativado/falha) confirma.
+- **Alias type quebra build:** `PostgresTransactionDatabase` é `server-only` mas type-only import é eliminado em build; confirmar `pnpm build` e `payload generate:types` sem erro.
+- **Outcome vazio colide com falha real da Graph API que também retorna status vazio:** não colide — falha real sempre escreve `failedInstagramSyncStatus` com `error`+`errorAt`, nunca `{}`; só o caminho não-configurado retorna `{}`.
+
+## Aceite de engenharia
+
+- [ ] Aceite de produto ainda coberto: botão só quando configured; rota 400 para não-configurado alinha impl doc ↔ código; tipo deduplicado sem mudar semântica de persist; painel inalterado visualmente
+- [ ] Invariantes AGENTS/engineering-standards: sem nova collection; sem Consent novo; `overrideAccess:false` preservado; rota mantém `isSameOriginRequest` + `payload.auth` users-only; alias type-only sem runtime coupling
+- [ ] Testes de domínio previstos: unit do sync (status vazio) + verificação manual da rota 400 vs 200; tsc gate
+
+Self-score decision-quality: 5/5 (opções + rejeitadas em 2 decisões caras, appetite respeitado, rabbit holes nomeados, reusa owner existente, outcome preservado).
diff --git a/src/app/(frontend)/api/social-feed/sync/route.ts b/src/app/(frontend)/api/social-feed/sync/route.ts
index a4a22a05..54585374 100644
--- a/src/app/(frontend)/api/social-feed/sync/route.ts
+++ b/src/app/(frontend)/api/social-feed/sync/route.ts
@@ -33,6 +33,15 @@ export async function POST(request: Request): Promise {
const outcome = await syncInstagramFeed(payload, {
signal: AbortSignal.timeout(INSTAGRAM_SYNC_TIMEOUT_MS),
})
+ // `syncInstagramFeed` returns `{ ok: false, status: {} }` only when
+ // `isInstagramFeedConfigured` is false (no token/ID) — no snapshot/status
+ // was persisted, so no cache to bust.
+ if (!outcome.ok && Object.keys(outcome.status).length === 0) {
+ return NextResponse.json(
+ { ok: false, error: 'Instagram não configurado — informe token e ID da conta.' },
+ { status: 400 },
+ )
+ }
revalidateTag(REVALIDATE_SOCIAL_FEED_TAG)
return NextResponse.json({ ok: outcome.ok, status: outcome.status })
} catch {
diff --git a/src/utilities/socialFeed/instagramFeed.ts b/src/utilities/socialFeed/instagramFeed.ts
index 13af5afe..e46addc9 100644
--- a/src/utilities/socialFeed/instagramFeed.ts
+++ b/src/utilities/socialFeed/instagramFeed.ts
@@ -1,6 +1,7 @@
import 'server-only'
import type { SocialFeedSetting } from '@/payload-types'
+import type { PostgresTransactionDatabase } from '@/utilities/postgresTransactionLocks'
import { sql } from '@payloadcms/db-postgres'
import type { Payload } from 'payload'
@@ -331,10 +332,13 @@ export const describeInstagramError = (cause: unknown): string => {
* database (`getPostgresTransactionDatabase`, S11) so the write lands inside
* the global save's transaction — writing the same row from a second pool
* connection while the save holds its row lock deadlocks on that lock.
+ *
+ * Owner is `PostgresTransactionDatabase` (`postgresTransactionLocks.ts`);
+ * this alias keeps the historical import path stable — prefer the owner type
+ * for new code.
+ * @deprecated Use `PostgresTransactionDatabase` directly.
*/
-export type InstagramPersistDatabase = {
- execute: (query: ReturnType) => Promise
-}
+export type InstagramPersistDatabase = PostgresTransactionDatabase
const countAffected = (result: unknown): number | null | undefined =>
(result as { rowCount?: number | null } | undefined)?.rowCount
diff --git a/src/utilities/socialFeed/instagramSync.ts b/src/utilities/socialFeed/instagramSync.ts
index a319dd42..e393d097 100644
--- a/src/utilities/socialFeed/instagramSync.ts
+++ b/src/utilities/socialFeed/instagramSync.ts
@@ -1,7 +1,10 @@
import 'server-only'
import type { SocialFeedSetting } from '@/payload-types'
-import { getPostgresTransactionDatabase } from '@/utilities/postgresTransactionLocks'
+import {
+ getPostgresTransactionDatabase,
+ type PostgresTransactionDatabase,
+} from '@/utilities/postgresTransactionLocks'
import {
describeInstagramError,
failedInstagramSyncStatus,
@@ -12,7 +15,6 @@ import {
persistInstagramSnapshot,
persistInstagramSyncStatus,
successInstagramSyncStatus,
- type InstagramPersistDatabase,
type InstagramSyncStatus,
} from '@/utilities/socialFeed/instagramFeed'
import type { Payload, PayloadRequest } from 'payload'
@@ -56,7 +58,7 @@ export const instagramCredentialsChanged = (
const resolvePersistDatabase = async (
payload: Payload,
req?: PayloadRequest,
-): Promise => {
+): Promise => {
if (!req?.transactionID) return null
try {
return await getPostgresTransactionDatabase(payload, req)