Skip to content

refactor(backend): remove billing, Stripe and plan limits - #52

Merged
patrickbrandao merged 7 commits into
mainfrom
feat/14-remove-billing-backend
Oct 11, 2026
Merged

patrickbrandao merged 7 commits into
mainfrom
feat/14-remove-billing-backend

Conversation

@patrickbrandao

Copy link
Copy Markdown
Owner

Closes #14

Removes the SaaS billing layer from the backend: Stripe, the Free/Pro plans, the per-request plan gate on the proxy, plan-based recording retention, billing emails and the schema behind them.

Merge order: merge after #51 (#12). On its own, this branch breaks 6 frontend unit suites (PlanPicker, Sidebar, Account, Register, Upgrade, billing-display) because packages/frontend/src/services/billing-display.ts on main still imports PLAN_LIMITS from manifest-shared, which this PR deletes. #51 deletes that file. I test-merged #51 into this branch locally: it merged cleanly, and on the merged tree the frontend vitest run (209 files, 4242 tests) and tsc --noEmit both passed. After #51 lands, rebase this branch and CI should be green.

Changes

Billing module. Deleted packages/backend/src/billing/ (PlanService, billing controller, billing emails, request-quota window, subscription status, webhook emails). Removed BillingModule from app.module.ts, routing/proxy/proxy.module.ts and analytics/analytics.module.ts. /api/v1/billing/* no longer exists.

Proxy hot path.

  • proxy.controller.ts: removed the planService.assertWithinRequestLimit gate. That gate was one DB lookup per request. Admission now goes straight to the local rate limiter and routing. Removed the PLAN_LIMIT_REQUESTS message mapping.
  • proxy-exception.filter.ts: removed the 402 PLAN_LIMIT_REQUESTS branch, which rendered the M204 upgrade message or an insufficient_quota 402. A 402 now takes the same generic path as any other 4xx.
  • Removed M204 from the error catalog and plan_request_limit_exceeded from the backend's blocked-request reasons. The shared classifier still recognizes the legacy class (see notes).
  • For normal requests, routing and error behavior are unchanged. On self-hosted installs the gate was already a no-op (isBillingEnabled() was false).

Auth. Removed the @better-auth/stripe plugin and getStripeClient from auth/auth.instance.ts. Dropped stripe and @better-auth/stripe from packages/backend/package.json and the lockfile. The frontend's own @better-auth/stripe entry is removed by #51.

Retention. request-recording-retention.service.ts keeps only the global path: REQUEST_RECORDING_RETENTION_DAYS, default 365. Removed SELECT_PLAN_EXPIRED_SQL, FREE/PRO_RETENTION_DAYS and isBillingEnabled. The 30-day default is #28.

Entities and shared. Dropped Tenant.limit_overrides and Tenant.billing_email_preferences, and deleted the TenantRequestUsage entity. Deleted packages/shared/src/plan-limits.ts and its exports.

Variables. Removed STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRO_PRICE_ID, PLAN_LIMIT_FREE_REQUESTS, PLAN_LIMIT_PRO_REQUESTS and PLAN_REQUEST_QUOTA_RESET_AT from the code and from packages/backend/.env.example. None of them were in docker/.

Migrations.

  • New 1803200000000-DropBillingSchema, registered in data-source-definitions.ts. It drops, in this order:

    1. the TRG_agent_messages_count_tenant_request_usage trigger and the count_tenant_request_usage() function;
    2. the tenant_request_usage_cutover_v1 row in backfill_state;
    3. requests.quota_counted;
    4. the tenant_request_usage and billing_email_logs tables;
    5. the tenants.limit_overrides and tenants.billing_email_preferences columns.

    The trigger must go first: on a database where the old cloud-mode migration ran, dropping only the table would make every attempt insert fail. Every statement uses IF EXISTS. down() restores the self-hosted schema: the two tenant columns, plus billing_email_logs and its indexes.

  • Minimal fix to 1801300000000-AddTenantRequestUsage.ts: it imported deleted billing helpers. Only its self-hosted path is kept, so up() is a no-op. The cutover-state constant is inlined for down(). It no longer imports isSelfHosted. No other old migration was changed.

  • The Better Auth Stripe plugin tables (subscription, user.stripeCustomerId) only existed with STRIPE_* set, so they are left alone.

Other. scripts/db-footprint-bench.mjs drops its tenant_request_usage query. docs/dev/manifest-guide.md is updated (structure tree, endpoint table, env vars, retention).

Tests

  • Removed: the billing/ specs, test/billing-request-limit.e2e-spec.ts, test/tenant-request-usage-migration.e2e-spec.ts, shared/__tests__/plan-limits.spec.ts and tenant-request-usage.entity.spec.ts.
  • Proxy unit tests:
    • proxy.controller.spec.ts: a normal request goes straight to rate limiting and routing. A 402 raised while routing is handled like any other 4xx, with the slot released and no plan reason or M204 recorded.
    • proxy-exception.filter.spec.ts: a 402 gets the generic envelope for SDK, chat and streaming clients, never shows an upgrade link, and records no blocked-request row.
  • New e2e test/proxy-no-plan-gate.e2e-spec.ts. With the old paywall switches set (STRIPE_* plus PLAN_LIMIT_FREE_REQUESTS=0), 3 requests to a stubbed OpenAI provider all return 200. A provider 402 is forwarded as source: provider, not as a plan block.
  • New unit spec and new e2e for DropBillingSchema1803200000000. The e2e runs the real migration registry from an empty DB, then reverts and re-applies the migration. It also simulates a live legacy quota trigger and checks that attempt inserts keep working after the drop.
  • Updated: auth.instance.spec.ts (no Stripe plugin), request-recording-retention.service.spec.ts (global path only), tenant.entity.spec.ts, the 1801300000000 migration spec, proxy-message-recorder.spec.ts and email-assets.spec.ts.

Local gates

Node 26.9 locally (CI uses 24). Postgres 16 ran in a disposable container, removed afterwards.

Gate Result
npm run lint 0 errors (392 warnings, all pre-existing)
backend tsc --noEmit -p tsconfig.json pass
backend build (npm run build --workspace=packages/backend, nest build) pass
backend unit (npm test --workspace=packages/backend) 504 suites / 9511 tests pass
shared unit / CLI unit 21 suites, 463 tests / 14 suites, 325 tests, all pass
backend e2e (npm run migration:run then npm run test:e2e, Postgres 16) 59/60 suites, 496/498 tests. The 2 failures are smoke-test ST-07 and ST-08 (notification limits). They fail the same way on origin/main (checked with the same DB), so they are pre-existing and unrelated.
key e2e suites, re-run on the final commit: drop-billing-schema-migration, proxy-no-plan-gate, proxy, proxy-fallback-auth, routing-flow, multi-key-providers 6 suites / 45 tests pass
migrations on an empty DB all 146 apply. Reverting DropBillingSchema1803200000000 restores billing_email_logs (with its PK and 2 indexes) and the two tenant columns. Re-running drops them again.
upgrade path migrated a fresh DB with origin/main code in its default mode, which created the real quota trigger, table, flag and cutover row, and inserted an attempt (counter = 1). Then ran this branch's migrations: every billing artifact was gone and a new attempt insert succeeded.
frontend, test-merged with #51 vitest 209 files / 4242 tests pass, tsc --noEmit pass

e2e note: --maxWorkers=2 cannot be combined with the script's --runInBand (Jest rejects the pair). Running Jest directly with 2 workers makes suites collide, because every suite runs dropSchema on the same database (missing relations, deadlocks). The run above therefore uses the repository's in-band script, exactly as CI does.

Needs qa-docker

Yes, this changes runtime behavior. Check that a chat completion through the mock provider passes, that GET /api/v1/billing/status returns 404, and that the boot works with no STRIPE_* variable.

Notes for follow-ups

🤖 Generated with Claude Code

patrickbrandao and others added 2 commits October 10, 2026 15:21
Thinker is self-hosted only, so the inherited Manifest Cloud UI goes.
The frontend leaves first so no screen breaks when #14/#15 drop the
matching backend endpoints.

- Billing/plans: delete PlanPicker, UpgradeSuccessModal,
  UsageLimitBanner, the /upgrade page, the Register ?step=plan picker,
  the plan stores, the plan-based range locks on the analytics pages,
  the plan-limit upgrade CTAs in the request views, the Account billing
  and usage-email sections, and the @better-auth/stripe client plugin
  and dependency.
- API Bot: delete the banner, the sidebar card, the dithered ground,
  their stylesheet and the SVG asset.
- GitHub stars: drop the header star button and its /api/v1/github call.
- Free Models: delete the page, its route, its API client and the assets
  only it used.
- Managed Gemini Free: drop the provider entry, icon alias, background
  /ensure provisioning call and the Calendly key URL.
- Cloud branches: checkIsSelfHosted() is gone and every screen behaves
  as self-hosted (Local providers always listed, local logs copy, CLI
  login pinned to this host, discovery after signup and setup). Drop the
  Manifest Cloud origin mapping, the cloud email card, the
  VITE_MANIFEST_SELFHOSTED read and the Dockerfile ENV that set it.
- Help: drop the Manifest Calendly booking link.
- Remove the dead CSS for all of the above, update the tests and the
  inherited dev guide.

Closes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Billing, Stripe and the Free/Pro plans belong to the SaaS layer that
Thinker drops (docs/decisoes-iniciais.md section 3).

- Delete packages/backend/src/billing (PlanService, billing controller,
  billing emails, request quota window, subscription status) and drop
  BillingModule from the app, proxy and analytics modules.
- Proxy: remove the per-request plan gate (assertWithinRequestLimit) and
  the PLAN_LIMIT_REQUESTS / M204 handling in the controller and the
  exception filter. The proxy no longer answers 402 on its own; a 402
  follows the generic error path.
- Auth: remove the @better-auth/stripe plugin; drop the stripe and
  @better-auth/stripe dependencies from the backend.
- Recording retention: keep only the global path (365-day default or
  REQUEST_RECORDING_RETENTION_DAYS); per-plan retention is gone.
- Tenant entity: drop limit_overrides and billing_email_preferences;
  delete the TenantRequestUsage entity.
- Shared: delete plan-limits and its exports.
- Remove the STRIPE_* / PLAN_LIMIT_* / PLAN_REQUEST_QUOTA_RESET_AT
  variables from .env.example.
- New migration DropBillingSchema1803200000000 drops the quota trigger
  and function, requests.quota_counted, tenant_request_usage,
  billing_email_logs and the two tenant columns. The old
  AddTenantRequestUsage migration keeps only its self-hosted path
  (a no-op up) so it no longer imports deleted billing code.

/api/v1/billing/* no longer exists, and the STRIPE_* and PLAN_LIMIT_*
variables are ignored if set.

Closes #14

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao
patrickbrandao changed the base branch from main to feat/12-remove-saas-dashboard October 11, 2026 02:33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao

Copy link
Copy Markdown
Owner Author

Revisão do PR #52: refactor(backend): remove billing, Stripe and plan limits (#14)

Diff revisado: git diff origin/feat/12-remove-saas-dashboard...b01370fddcf0c767b89e44b197645d84665bddb2. O PR está empilhado sobre o #51.

Topologia. O head b01370f é um merge de 5f0c407 (head do #51) em d4abfb2, o commit de trabalho do #14. A merge-base com a base do PR é 5f0c407. Como feat/12 foi criado sobre 74badb5, e o d4abfb2 está sobre 5e2530e, o diff contra a base também traz o #45 (docs/decisoes-iniciais.md) e o #44 (.github/**, codecov.yml). Esses dois já estão na main, revisados e mergeados lá, então não contam como mudança do #14. Por isso, nos pontos do #14, revisei git diff 5e2530e d4abfb2 (66 arquivos, +632/−5126). Conferi que o merge é limpo: git diff d4abfb2 b01370f é idêntico, linha a linha, a git diff 74badb5 5f0c407. Só mudam os offsets dos hunks em manifest-guide.md e no lockfile, e não houve resolução manual. Na aba "Files changed" do GitHub, as mudanças de workflow e de governança vão aparecer até o rebase. Isso é efeito da pilha, não deste PR.

CI e gates

  • gh pr checks 52: nenhum check. O ci.yml (.github/workflows/ci.yml:11-15) só dispara em pull_request para main, e a base deste PR é feat/12-remove-saas-dashboard. O CI mínima provisória #38 está fechado, então o gate é a CI verde, e este SHA nunca vai ter CI. Depois que o refactor(frontend): remove the SaaS layer from the dashboard #51 entrar, o PR precisa de rebase sobre a main, como o próprio autor prevê. Esse rebase gera um SHA novo, que precisa de CI verde e de nova revisão.
  • O relatório local do autor está completo (lint, tsc, build, unit de backend/shared/cli, e2e com Postgres 16, migrations do zero e upgrade a partir da main). Rodei de novo, neste head, tsc --noEmit do backend (limpo) e as specs afetadas: proxy.controller, proxy-exception.filter, streaming-abort, as duas migrations, auth.instance, retention, tenant.entity e common/errors. Resultado: 10 suítes, 269 testes, tudo verde.
  • Falhas do ST-07/ST-08: na main, a CI passou o smoke-test. A falha local do autor também aparece na main, então provavelmente vem do ambiente (Node 26 contra 24, ou fuso horário com period: 'day'). Mesmo assim, o ST-08 cobre o bloqueio por hard limit (M200) no proxy, e esse ponto ainda não foi verificado com este código em lugar nenhum. O Remover billing, Stripe e limites de plano do backend #14 não toca esse caminho: o gate removido ficava antes do try dos rate limiters, e o check de limite está no proxy service, que não mudou. O risco é baixo, mas a CI do PR rebaseado precisa mostrar o smoke-test verde (veja o should-fix 2).
  • qa-docker: pendente (o PR ainda é draft). O teste precisa cobrir: chat completion via mock, GET /api/v1/billing/status respondendo 404 e boot sem STRIPE_*.

Blockers

Nenhum.

Risco alto

Dois critérios se aplicam:

  1. Migration que destrói dados. 1803200000000-DropBillingSchema.ts:26-41 faz drop de tenant_request_usage, billing_email_logs, requests.quota_counted, tenants.limit_overrides e tenants.billing_email_preferences, e apaga a linha de cutover em backfill_state. O down() recria só o schema, sem as linhas. Isso está de acordo com a política de dados do Remover billing, Stripe e limites de plano do backend #14 (não existe instalação do Thinker), mas a decisão é do mantenedor.
  2. Muda auth. auth/auth.instance.ts:178 passa a montar o Better Auth sempre com [jwt(), ...mcp]: saem o plugin @better-auth/stripe, o getStripeClient e os callbacks de webhook. Não achei regressão. O plugin só era montado com billing ligado (cloud + STRIPE_*), então o self-hosted e a imagem Docker não mudam. Com ele saem as rotas /api/auth/stripe/webhook e /api/auth/subscription/*. database e secret continuam em uso, e nenhum código lê subscription nem stripeCustomerId.

1. Hot path do proxy

  • proxy.controller.ts:342-345 (head): a admissão vai direto para checkLimit → checkIpLimit → acquireSlot → proxyRequest, dentro do try de antes. Saiu uma consulta ao banco por requisição. O comportamento de erro das requisições normais não muda: os erros de rate limit e de roteamento continuam no handleProxyError de antes. Some só o caminho de lookup do plano que falhava (M500), porque o lookup deixou de existir.

  • proxy-exception.filter.ts: o ramo que saiu só disparava com HttpException 402 cujo corpo trazia code === 'PLAN_LIMIT_REQUESTS'. Na 5e2530e, o único produtor desse código era o plan.service.ts:454. Os 402 reais dos provedores não passam por esse filtro: são encaminhados pelo response handler, com source: provider, e isso não mudou. Agora um 402 que chegue ao filtro segue o caminho genérico de 4xx, e não sobra nenhum produtor de 402 no src do backend (conferi com grep).

  • Cobertura: a mudança de tratamento de erro no hot path está coberta nos níveis certos.

    • Unit proxy.controller.spec.ts:1056-1108: a ordem rate limit → roteamento sem consulta de plano, e um 402 vindo do roteamento tratado localmente, com o slot liberado e sem M204.
    • Unit proxy-exception.filter.spec.ts:365-410: envelope genérico para SDK, chat e stream, sem link de upgrade e sem linha de bloqueio.
    • e2e test/proxy-no-plan-gate.e2e-spec.ts: 3 requisições passam mesmo com as variáveis antigas de paywall (PLAN_LIMIT_FREE_REQUESTS=0), e um 402 do provedor chega como source: provider e é gravado com error_http_status = 402, sem M204.

    Não é blocker.

2. Migrations

  • DropBillingSchema1803200000000:
    • A ordem está correta. Primeiro sai o trigger e depois a função, antes da coluna quota_counted e da tabela que o trigger escreve. Sem isso, o DROP FUNCTION falharia por dependência, ou todo insert de tentativa quebraria.
    • Todo DROP usa IF EXISTS. O DELETE é idempotente.
    • A migration roda numa transação (migrationsTransactionMode: 'each', database.module.ts:52).
    • O down() recria billing_email_logs com as mesmas colunas, PK e os dois índices do 1798200000000-AddBillingEmailLogs.ts, e as duas colunas jsonb dos 1798100000000 e 1798300000000. Isso equivale ao schema self-hosted, que nunca teve o contador de quota.
    • Em banco migrado em modo cloud, a migration é segura. O e2e drop-billing-schema-migration.e2e-spec.ts roda o registro real do zero, faz revert e reaplica (idempotente), e simula um trigger legado vivo, com inserts funcionando depois do drop. O autor também testou o upgrade real a partir da main.
    • Não sobra nenhuma referência às tabelas ou colunas fora das migrations antigas.
  • Edição de 1801300000000-AddTenantRequestUsage.ts: o up() virou no-op, que é o caminho self-hosted, como o Remover billing, Stripe e limites de plano do backend #14 permite. Em banco onde ela já rodou, o TypeORM não executa de novo. Se ela rodou no caminho cloud, quem limpa é a 1803200000000. Se ainda não rodou, o no-op seguido do drop idempotente dá o mesmo schema final. O down() continua idempotente, com a constante de cutover inline. É seguro.
  • Atenção: no código antigo, isSelfHosted() tinha cloud como padrão fora de contêiner (detect-self-hosted.ts:14). Bancos criados com npm run migration:run fora do Docker caíram no caminho cloud. Por isso a ordem trigger → tabela não é um detalhe teórico, e ela está coberta.

3. M204 / plan_request_limit_exceeded

  • Saíram o M204 do catálogo (error-codes.ts), o motivo plan_request_limit_exceeded de MANIFEST_BLOCKED_REQUEST_REASONS e o mapeamento M204 de MANIFEST_CODE_TO_REASON. As specs de catálogo (common/errors) passam.
  • Linhas antigas com error_code = 'M204' não quebram nada. O isManifestErrorCode (error-codes.ts:113) devolve null para código desconhecido, e não há lookup dinâmico de MANIFEST_ERRORS[row.error_code].
  • A sobra em packages/shared/src/error-taxonomy.ts:153,184-187 (e nas fixtures do frontend e do shared) é inofensiva e só classifica linhas legadas. Veja o nit 1.

4. Stripe no Better Auth

Comentado em "Risco alto". stripe e @better-auth/stripe saíram do packages/backend/package.json. Sobra no lockfile; veja o should-fix 1.

Segurança

  • Nenhum destino de saída novo. Sai o tráfego para a API da Stripe.
  • Nenhuma credencial é logada ou exposta.
  • A retenção fica só com o caminho global (request-recording-retention.service.ts, padrão de 365 dias): mesmo comportamento de antes no self-hosted e sem tocar em isolamento entre tenants. Ela é instance-wide, como já era.
  • Escopo de tenant e checagem de papel não mudaram.

Should-fix

  1. package-lock.json:15144: node_modules/stripe ficou órfão no head. Cada branch removeu o seu dependente (o backend aqui, o frontend no refactor(frontend): remove the SaaS layer from the dashboard #51), mas nenhum dos dois removeu o pacote raiz, e o merge preservou a entrada. Reproduzi recalculando o lock deste head numa cópia isolada com npm install --package-lock-only: a única diferença é a remoção de node_modules/stripe. O npm ci poda o extraneous, então não quebra o build. Mas o lock fica sujo, e o próximo npm install de qualquer PR vai carregar esse diff. Correção: no rebase sobre a main, depois do refactor(frontend): remove the SaaS layer from the dashboard #51, rodar npm install e commitar o lock sem a entrada.
  2. Gate de CI e e2e do proxy. Ver "CI e gates". Este SHA não tem CI e não pode ser mergeado por --match-head-commit com o gate de CI cumprido. O ST-08 (hard limit no proxy) falha localmente e não roda em CI neste PR. Correção: rebasear sobre a main depois do refactor(frontend): remove the SaaS layer from the dashboard #51 e confirmar que a CI do PR fica verde, incluindo Backend e2e com o smoke-test. Abrir uma issue para a falha local do ST-07/ST-08, que o autor já sugere. O motivo é que hoje o e2e local não serve como evidência para mudanças no caminho de limites.

Nits

  1. packages/shared/src/error-taxonomy.ts:153,184: plan_request_limit_exceeded continua em ERROR_CLASSES e no mapa de motivos, e as fixtures do frontend (MessageDetails.test.tsx:125, MessageTableStatus.test.tsx:40) também usam o valor. Como não existe instalação do Thinker com linhas legadas, dá para remover numa limpeza posterior, com as migrations antigas 1798000000000/1800100000000/1800200000000 mantidas. Sugiro registrar numa issue (v0.1).
  2. O critério de aceite do grep do Remover billing, Stripe e limites de plano do backend #14 não é cumprido ao pé da letra. Ainda casam crm-metrics/*.spec.ts (fixtures stripe.com, que saem com o Remover waitlist, CRM, Gemini Free, free-models, GitHub stars e páginas de erro públicas #15), scoring/specificity-signals.ts:22 e duas specs de scoring (texto de prompt de exemplo) e o comentário da nova migration. São falsos positivos, não código de billing. Vale citar o Remover waitlist, CRM, Gemini Free, free-models, GitHub stars e páginas de erro públicas #15 no PR para fechar o critério.
  3. 1803200000000-DropBillingSchema.ts:26-41: diferente da 1801300000000, não define lock_timeout. O DROP TRIGGER em agent_messages e o ALTER TABLE requests pegam locks fortes e, num banco ocupado, esperariam indefinidamente no boot. Sem instalações existentes, o impacto é nulo hoje. Fica opcional, com SET LOCAL lock_timeout.
  4. Changeset: não se aplica agora. manifest-backend e manifest-shared estão em ignore no .changeset/config.json, e o changelog inicial da 0.1.0 é do Release, Changesets 0.1.0 e publicação no Docker Hub #31, como combinado no refactor(frontend): remove the SaaS layer from the dashboard #51. A remoção de /api/v1/billing/* e das variáveis STRIPE_*/PLAN_LIMIT_* deve entrar nas notas da 0.1.0.
  5. test/proxy-no-plan-gate.e2e-spec.ts:121: o stub de fetch repassa ao fetch real qualquer host que não seja api.openai.com. Isso não muda o produto, mas um e2e que não depende de rede deveria falhar fechado (devolver erro em vez de sair para a rede).

Conclusão

Nada bloqueia. O código do #14 está correto e o hot path está coberto por testes unitários e e2e. A migration é segura em bancos self-hosted e cloud, e é reversível no schema. O PR é de risco alto por dois critérios: migration destrutiva e mudança no auth instance. O merge é decisão do mantenedor, depois de três condições: o rebase sobre a main (pós-#51), CI verde nesse novo SHA, com nova revisão, e o qa-docker. Os should-fixes 1 e 2 precisam ser corrigidos no PR ou registrados em issue com link no PR antes do merge.

Head revisado: b01370f
Veredito: risco alto (aguardar mantenedor)

@patrickbrandao

Copy link
Copy Markdown
Owner Author

Destino dos should-fixes da revisão (ADR 0002):

  1. Entrada órfã node_modules/stripe no lockfile: corrigida depois que o refactor(frontend): remove the SaaS layer from the dashboard #51 entrar. A branch será atualizada com a main, o lockfile regenerado com npm install e o resultado commitado.
  2. Gate de CI e e2e: a ci.yml só roda em PRs contra a main. Quando o refactor(frontend): remove the SaaS layer from the dashboard #51 entrar, este PR é reapontado para a main e atualizado, a CI roda (incluindo o smoke-test) e o PR passa por uma nova revisão do novo head. As falhas locais ST-07/ST-08 estão na E2E de limites (ST-07/ST-08) falham fora de UTC #53, com correção em andamento.

Nits: a sobra em error-taxonomy.ts e o lock_timeout da migração entram na #15 (a próxima migração de remoção). O stub de fetch que cai na rede real para outros hosts fica registrado para a #34, que testa a ausência de egress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from feat/12-remove-saas-dashboard to main October 11, 2026 16:58
patrickbrandao and others added 2 commits October 11, 2026 13:59
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main now holds the squash of #51, whose tree equals ce9fd33, which this
branch already merged, so the result keeps this tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao

Copy link
Copy Markdown
Owner Author

Re-revisão: PR #52 depois do merge do #51 (head 061fbe1)

Antes revisado: b01370f. Agora: 061fbe15fad2ffe2e3b6656ee6691ba693d033b8, base main (4d861c6).

Topologia e equivalência

Resoluções de conflito

Código novo da main contra o que o PR remove

Busquei no head todos os exports dos 31 arquivos deletados, com git grep -w. São 52 símbolos, entre eles PlanService, PLAN_LIMITS, FREE_PLAN_REQUESTS_PER_MONTH, BillingStatus, TenantRequestUsage, getStripeClient, isBillingEnabled, REQUEST_QUOTA_RESET_AT_ENV, requestQuotaWindowStartMs e REQUEST_USAGE_CUTOVER_STATE. Também busquei os caminhos billing/, plan-limits, request-quota-window, @better-auth/stripe, STRIPE_* e PLAN_LIMIT_*.

CI e execuções locais neste head

  • CI em 061fbe1: 11 check-runs, todos completed/success: Lint, Unit (backend/frontend/shared/cli), Backend e2e (PostgreSQL 16), CodeQL (x2), dependency-review, Compose smoke test e install.sh end-to-end. O Backend e2e roda migration:run mais toda a suíte e2e, inclusive o smoke-test (o ST-07/ST-08 foi corrigido pelo E2E de limites (ST-07/ST-08) falham fora de UTC #53/fix(backend): count same-second usage in limit windows #56). O should-fix 2 da revisão anterior está resolvido.

  • Local: o ci.yml provisório não roda typecheck nem build, então rodei neste head:

    Comando Resultado
    npm ci passa
    build do shared passa
    tsc --noEmit do backend passa
    tsc --noEmit do frontend passa

Blockers

Nenhum.

Should-fix

  1. packages/backend/test/drop-billing-schema-migration.e2e-spec.ts:134-142 contorna o guard do fix(backend): refuse e2e dropSchema outside an explicit test database #69. O DataSource usa dropSchema: true, com url: process.env['DATABASE_URL'] ?? 'postgresql://myuser:mypassword@localhost:5432/mydatabase'. Foi exatamente esse padrão que o fix(backend): refuse e2e dropSchema outside an explicit test database #69 removeu de todos os outros specs. O contrato dele (e2e-global-setup.ts) diz: "Every spec also resolves its URL through the same guard".
    • Hoje: o globalSetup do jest-e2e.json aborta a execução antes, então pelo npm run test:e2e o fallback não é alcançável.
    • Cenário de falha: alguém roda o arquivo com outra config do jest, ou o globalSetup muda. Aí o spec apaga o schema de localhost:5432/mydatabase, o banco local de desenvolvimento (issue E2E: impedir dropSchema fora de banco de teste explícito #67).
    • Correção: import { resolveE2eDatabaseUrl } from './e2e-db-guard'; e url: resolveE2eDatabaseUrl(),.

Nits

  1. packages/frontend/tests/pages/ProviderOverviewPages.test.tsx:4,358-362, Workspace.test.tsx:133-134 e Workspace-validation.test.tsx:101-102 ainda mockam FREE_PLAN_REQUESTS_PER_MONTH e PLAN_LIMITS no vi.mock('manifest-shared'). ProviderOverviewPages.test.tsx:4 ainda importa o símbolo, que não existe mais no manifest-shared real. Isso vem de antes, não está no diff, e não quebra nada: tests está fora do tsconfig do frontend, e o Unit (frontend) passou. Dá para limpar no Remover waitlist, CRM, Gemini Free, free-models, GitHub stars e páginas de erro públicas #15 ou no Rebranding do dashboard #25.
  2. Continuam valendo os nits 1, 3 e 5 da revisão anterior:
    • plan_request_limit_exceeded em error-taxonomy.ts;
    • o lock_timeout na 1803200000000;
    • o stub de fetch do proxy-no-plan-gate.e2e-spec.ts, que deveria falhar fechado.

Segurança

Não muda nada em relação à revisão anterior. Não há destino de saída novo (sai a Stripe). Nenhuma credencial é logada nem exposta, e o escopo de tenant e as checagens de papel ficam intactos.

Risco alto

Os dois critérios da revisão anterior continuam valendo:

  1. Migration que destrói dados. 1803200000000-DropBillingSchema.ts:28-38 apaga o trigger e a função de contagem, requests.quota_counted, tenant_request_usage, billing_email_logs e tenants.limit_overrides/billing_email_preferences. O down() recria só o schema.
  2. Muda auth. auth/auth.instance.ts:173 (buildPlugins) sem o plugin @better-auth/stripe, agora sobre o better-auth 1.7.7.

Gates

Head revisado: 061fbe1
Veredito: risco alto (aguardar mantenedor)

@patrickbrandao

Copy link
Copy Markdown
Owner Author

qa-docker: PASS

Tested SHA: 061fbe15fad2ffe2e3b6656ee6691ba693d033b8 (head confirmed with gh pr view; base is current main 4d861c6). Image thinker:qa-9aa1a0 built from docker/Dockerfile on arm64 in 44 s (warm cache), 458 MB. Stack: isolated Compose project thinker-qa-9aa1a0, volumes renamed through an override (name: !reset null), resolved names carried the run id, labels checked before down -v, nothing of the run left behind. Postgres 16.15 (postgres:16-alpine@sha256:721873c3...). Base images: node:24-alpine (build), node:24-slim (prod deps), gcr.io/distroless/nodejs24-debian13:nonroot (runtime), all by digest.

Step Result
Build pass (44 s)
Stack up on a fresh volume, /api/v1/health pass (healthy in about 11 s); migrations ran clean, last applied is DropBillingSchema1803200000000; no tenant_request_usage, billing_email_logs or subscription tables, and tenants has no limit_overrides / billing_email_preferences
Dashboard serves HTML pass
First-admin setup (201), second setup refused (400), wrong password 401, sign-in 200, session pass
Agent creation returns a key; custom provider pointing to a host mock pass
Gateway through the host mock: /v1/models, /v1/chat/completions (JSON, SSE), /v1/messages (Bearer, x-api-key, SSE), /v1/responses (JSON, SSE), no key gives 401 pass, all 200
No plan-limit behavior: 60 extra mixed requests (chat, messages, responses) pass, 60 x 200, no quota error; all 67 recorded in agent_messages; no quota/limit/billing/stripe lines in the app log
Billing endpoints gone: /api/v1/billing/{status,plan,email-preferences,checkout}, /api/v1/billing, /api/v1/stripe/webhook, /api/auth/stripe/webhook, /api/v1/plan, /api/v1/limits (GET, POST, PATCH, signed in) pass, all 404
Contracts contracts/gateway against the container pass, 6/6
Backend e2e: TZ=UTC npm run test:e2e:docker --workspace=packages/backend (shared built first) pass, 64 suites, 540 tests, 135 s (147 s with container startup); disposable Postgres removed
Teardown (down -v with -p, label check first) pass

Observations (nothing flaky, nothing needed a retry):

  • On the first boot with an empty database, Better Auth logs ERROR ... Database schema mismatch before its tables exist. Cosmetic, identical on main.
  • Boot logs modelparams catalog fetch returned 403: the public-catalog sync from ADR 0003, not related to this PR.
  • Upgrade path (a database that already ran the old cloud-mode quota migration) was not exercised in Docker in this run; it is covered by drop-billing-schema-migration.e2e-spec.ts, which passed in the e2e suite.

Refs #67

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@patrickbrandao

Copy link
Copy Markdown
Owner Author

qa-docker: addendum for the new head 1213ffec71c8bbd648d433bdb822722a6f08b7d2

The PR head moved from 061fbe1 to 1213ffe while I was testing. The only change is packages/backend/test/drop-billing-schema-migration.e2e-spec.ts (it now uses resolveE2eDatabaseUrl()), confirmed with git diff 061fbe1 1213ffe.

  • The Docker image (thinker:qa-9aa1a0), the stack, the smoke and the contracts in my earlier comment were built and run on 061fbe15fad2ffe2e3b6656ee6691ba693d033b8. The change between the two SHAs is a test file only, so the image content is the same.
  • The backend e2e (TZ=UTC npm run test:e2e:docker --workspace=packages/backend) was run on both SHAs:
SHA Result
061fbe1 pass, 64 suites, 540 tests, 135 s
1213ffe pass, 64 suites, 540 tests, 89 s

Disposable Postgres container removed after the run. No flake on either run.

@patrickbrandao

Copy link
Copy Markdown
Owner Author

Re-revisão: PR #52, delta 061fbe1 → 1213ffe

Antes revisado: 061fbe15fad2ffe2e3b6656ee6691ba693d033b8 (revisão). Agora: 1213ffec71c8bbd648d433bdb822722a6f08b7d2. Revisei num worktree destacado nesse SHA.

Delta

git diff 061fbe1 1213ffe tem um commit só (test(backend): route the billing drop e2e through the database guard), que mexe num arquivo só:

  • packages/backend/test/drop-billing-schema-migration.e2e-spec.ts:5 importa resolveE2eDatabaseUrl de ./e2e-db-guard.
  • drop-billing-schema-migration.e2e-spec.ts:137-138: o DataSource com dropSchema: true usa url: resolveE2eDatabaseUrl(). O fallback postgresql://myuser:mypassword@localhost:5432/mydatabase saiu.

resolveE2eDatabaseUrl() lança erro antes de qualquer conexão nestes casos:

  • DATABASE_URL ausente;
  • o nome do banco não tem test como palavra separada;
  • THINKER_E2E_ALLOW_DROP=1 ausente.

O should-fix 1 da revisão anterior está resolvido.

Busca por outros desvios do guard no PR inteiro

  • Em git diff origin/main...1213ffe, as únicas linhas adicionadas com new DataSource( ou dropSchema são as do spec acima. Também não há DATABASE_URL com fallback, nem DROP SCHEMA, DROP DATABASE ou TRUNCATE novos.
  • Os outros arquivos de teste do PR:
    • proxy-no-plan-gate.e2e-spec.ts (novo) obtém o DataSource via app.get(DataSource), depois do createTestApp(), e helpers.ts:160 usa resolveE2eDatabaseUrl();
    • helpers.ts (modificado) continua no guard;
    • billing-request-limit.e2e-spec.ts e tenant-request-usage-migration.e2e-spec.ts foram deletados.
  • No head, todo spec em packages/backend/test/ que abre new DataSource(...) com dropSchema: true importa e usa resolveE2eDatabaseUrl(). Também conferi os specs que estão fora do diff. O globalSetup do jest-e2e.json continua apontando para e2e-global-setup.ts.
  • scripts/db-footprint-bench.mjs só perde a query do contador de quota.

CI neste head

11 check-runs, todos completed/success:

  • Lint;
  • Unit (backend, frontend, shared e cli);
  • Backend e2e (PostgreSQL 16), que inclui o spec alterado;
  • CodeQL (2 runs);
  • dependency-review;
  • Compose smoke test (read-only);
  • install.sh end-to-end.

Workflow runs: CI, Docker Smoke, CodeQL e Dependency review.

Blockers

Nenhum.

Should-fix

Nenhum em aberto.

Nits

Continuam valendo os nits da revisão anterior. Nenhum bloqueia:

  • os mocks de FREE_PLAN_REQUESTS_PER_MONTH/PLAN_LIMITS nos testes do frontend;
  • plan_request_limit_exceeded em error-taxonomy.ts;
  • o lock_timeout na 1803200000000;
  • o stub de fetch do proxy-no-plan-gate.e2e-spec.ts, que deveria falhar fechado.

Segurança

O delta só muda um teste. A análise da revisão anterior continua valendo:

  • não há destino de saída novo;
  • nenhuma credencial é logada ou exposta;
  • o escopo de tenant e as checagens de papel ficam intactos.

Risco alto

Os dois critérios da revisão anterior continuam valendo, porque o delta não muda nada disso:

  1. Migration que destrói dados. 1803200000000-DropBillingSchema.ts apaga tabelas, colunas, o trigger e a função de contagem de billing. O down() recria só o schema, sem os dados.
  2. Muda auth. auth/auth.instance.ts (buildPlugins) roda sem o plugin @better-auth/stripe.

Gates

  • CI: verde em 1213ffe.
  • qa-docker: o delta não muda runtime (só um spec e2e). O qa-docker pedido na revisão anterior para a árvore de runtime de 061fbe1 vale para este head, porque o runtime não mudou. Se ele ainda não rodou, continua pendente.
  • Merge: é decisão do mantenedor (ADR 0002). A revisão vale só para o SHA abaixo, e qualquer commit novo exige nova revisão.

Head revisado: 1213ffe
Veredito: risco alto (aguardar mantenedor)

@patrickbrandao
patrickbrandao merged commit df6ca96 into main Oct 11, 2026
11 checks passed
@patrickbrandao
patrickbrandao deleted the feat/14-remove-billing-backend branch October 11, 2026 17:23
patrickbrandao added a commit that referenced this pull request Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
patrickbrandao added a commit that referenced this pull request Oct 12, 2026
…base guard (#111)

* test(backend): fail the unit suite when an e2e spec bypasses the database guard

Add test/e2e-db-guard-scan.ts and its spec, which run in the unit suite
and parse every .ts file under packages/backend/test with the TypeScript
AST. They fail when a file:

- wipes the database (dropSchema not literally false, DROP SCHEMA or
  DROP DATABASE SQL, .dropDatabase(), .synchronize(true)) without
  calling resolveE2eDatabaseUrl() or createTestApp();
- falls back from process.env.DATABASE_URL with ?? or ||;
- has a string naming localhost:5432 or mydatabase (e2e-db-guard.spec.ts
  is exempt, since it proves the guard refuses those URLs).

Fixtures under test/fixtures/e2e-db-guard (*.fixture, outside the scan
and outside Jest) reproduce the legacy migration-spec pattern from #52,
#60 and #66, a raw DROP SCHEMA reset, and the accepted guarded pattern.
A temp-dir tree scan proves that adding the legacy spec fails the check.

Closes #108

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(backend): treat a queryRunner.dropSchema() call as a wipe in the guard scan

Refs #108

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remover billing, Stripe e limites de plano do backend

1 participant