Repository navigation
refactor(backend): remove billing, Stripe and plan limits - #52
Conversation
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revisão do PR #52: refactor(backend): remove billing, Stripe and plan limits (#14)Diff revisado: Topologia. O head CI e gates
BlockersNenhum. Risco altoDois critérios se aplicam:
1. Hot path do proxy
2. Migrations
3. M204 /
|
|
Destino dos should-fixes da revisão (ADR 0002):
Nits: a sobra em |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-revisão: PR #52 depois do merge do #51 (head 061fbe1)Antes revisado: Topologia e equivalência
Resoluções de conflito
Código novo da
|
| Comando | Resultado |
|---|---|
npm ci |
passa |
build do shared |
passa |
tsc --noEmit do backend |
passa |
tsc --noEmit do frontend |
passa |
Blockers
Nenhum.
Should-fix
packages/backend/test/drop-billing-schema-migration.e2e-spec.ts:134-142contorna o guard do fix(backend): refuse e2e dropSchema outside an explicit test database #69. ODataSourceusadropSchema: true, comurl: 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
globalSetupdojest-e2e.jsonaborta a execução antes, então pelonpm run test:e2eo fallback não é alcançável. - Cenário de falha: alguém roda o arquivo com outra config do jest, ou o
globalSetupmuda. Aí o spec apaga o schema delocalhost: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';eurl: resolveE2eDatabaseUrl(),.
- Hoje: o
Nits
packages/frontend/tests/pages/ProviderOverviewPages.test.tsx:4,358-362,Workspace.test.tsx:133-134eWorkspace-validation.test.tsx:101-102ainda mockamFREE_PLAN_REQUESTS_PER_MONTHePLAN_LIMITSnovi.mock('manifest-shared').ProviderOverviewPages.test.tsx:4ainda importa o símbolo, que não existe mais nomanifest-sharedreal. Isso vem de antes, não está no diff, e não quebra nada:testsestá fora dotsconfigdo 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.- Continuam valendo os nits 1, 3 e 5 da revisão anterior:
plan_request_limit_exceededemerror-taxonomy.ts;- o
lock_timeoutna1803200000000; - o stub de
fetchdoproxy-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:
- Migration que destrói dados.
1803200000000-DropBillingSchema.ts:28-38apaga o trigger e a função de contagem,requests.quota_counted,tenant_request_usage,billing_email_logsetenants.limit_overrides/billing_email_preferences. Odown()recria só o schema. - Muda auth.
auth/auth.instance.ts:173(buildPlugins) sem o plugin@better-auth/stripe, agora sobre o better-auth 1.7.7.
Gates
- CI: verde neste head.
qa-docker: precisa rodar de novo neste head. A árvore de runtime mudou em relação ab01370f: agora inclui o x-api-key (feat(gateway): accept x-api-key as an agent key transport #63), os servidores locais (fix(backend): route chat for built-in LM Studio and llama.cpp providers #100/fix(providers): reserve local runner aliases and validate their base URLs #103), os headers do Playground (fix(playground): block credential headers in Playground extra headers #104/fix(providers): make adapter headers win over Playground extra headers #107) e o better-auth 1.7.7 (chore(deps): bump the better-auth group across 1 directory with 5 updates #5) junto com a remoção do plugin Stripe. Roteiro:- boot sem
STRIPE_*; migration:runnum volume novo;- chat completion via mock com
Authorizatione comx-api-key; GET /api/v1/billing/statusrespondendo 404;- login e sessão no dashboard.
- boot sem
- Should-fix: pela regra do
aprovado, cada should-fix precisa ser corrigido no PR ou registrado numa issue com link no PR antes do merge. Um commit novo exige nova revisão. - O merge é decisão do mantenedor (ADR 0002).
Head revisado: 061fbe1
Veredito: risco alto (aguardar mantenedor)
qa-docker: PASSTested SHA:
Observations (nothing flaky, nothing needed a retry):
|
Refs #67 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
qa-docker: addendum for the new head
|
| 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.
Re-revisão: PR #52, delta
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
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.
Changes
Billing module. Deleted
packages/backend/src/billing/(PlanService, billing controller, billing emails, request-quota window, subscription status, webhook emails). RemovedBillingModulefromapp.module.ts,routing/proxy/proxy.module.tsandanalytics/analytics.module.ts./api/v1/billing/*no longer exists.Proxy hot path.
proxy.controller.ts: removed theplanService.assertWithinRequestLimitgate. That gate was one DB lookup per request. Admission now goes straight to the local rate limiter and routing. Removed thePLAN_LIMIT_REQUESTSmessage mapping.proxy-exception.filter.ts: removed the 402PLAN_LIMIT_REQUESTSbranch, which rendered the M204 upgrade message or aninsufficient_quota402. A 402 now takes the same generic path as any other 4xx.M204from the error catalog andplan_request_limit_exceededfrom the backend's blocked-request reasons. The shared classifier still recognizes the legacy class (see notes).isBillingEnabled()was false).Auth. Removed the
@better-auth/stripeplugin andgetStripeClientfromauth/auth.instance.ts. Droppedstripeand@better-auth/stripefrompackages/backend/package.jsonand the lockfile. The frontend's own@better-auth/stripeentry is removed by #51.Retention.
request-recording-retention.service.tskeeps only the global path:REQUEST_RECORDING_RETENTION_DAYS, default 365. RemovedSELECT_PLAN_EXPIRED_SQL,FREE/PRO_RETENTION_DAYSandisBillingEnabled. The 30-day default is #28.Entities and shared. Dropped
Tenant.limit_overridesandTenant.billing_email_preferences, and deleted theTenantRequestUsageentity. Deletedpackages/shared/src/plan-limits.tsand its exports.Variables. Removed
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_PRO_PRICE_ID,PLAN_LIMIT_FREE_REQUESTS,PLAN_LIMIT_PRO_REQUESTSandPLAN_REQUEST_QUOTA_RESET_ATfrom the code and frompackages/backend/.env.example. None of them were indocker/.Migrations.
New
1803200000000-DropBillingSchema, registered indata-source-definitions.ts. It drops, in this order:TRG_agent_messages_count_tenant_request_usagetrigger and thecount_tenant_request_usage()function;tenant_request_usage_cutover_v1row inbackfill_state;requests.quota_counted;tenant_request_usageandbilling_email_logstables;tenants.limit_overridesandtenants.billing_email_preferencescolumns.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, plusbilling_email_logsand its indexes.Minimal fix to
1801300000000-AddTenantRequestUsage.ts: it imported deleted billing helpers. Only its self-hosted path is kept, soup()is a no-op. The cutover-state constant is inlined fordown(). It no longer importsisSelfHosted. No other old migration was changed.The Better Auth Stripe plugin tables (
subscription,user.stripeCustomerId) only existed withSTRIPE_*set, so they are left alone.Other.
scripts/db-footprint-bench.mjsdrops itstenant_request_usagequery.docs/dev/manifest-guide.mdis updated (structure tree, endpoint table, env vars, retention).Tests
billing/specs,test/billing-request-limit.e2e-spec.ts,test/tenant-request-usage-migration.e2e-spec.ts,shared/__tests__/plan-limits.spec.tsandtenant-request-usage.entity.spec.ts.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.test/proxy-no-plan-gate.e2e-spec.ts. With the old paywall switches set (STRIPE_*plusPLAN_LIMIT_FREE_REQUESTS=0), 3 requests to a stubbed OpenAI provider all return 200. A provider 402 is forwarded assource: provider, not as a plan block.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.auth.instance.spec.ts(no Stripe plugin),request-recording-retention.service.spec.ts(global path only),tenant.entity.spec.ts, the1801300000000migration spec,proxy-message-recorder.spec.tsandemail-assets.spec.ts.Local gates
Node 26.9 locally (CI uses 24). Postgres 16 ran in a disposable container, removed afterwards.
npm run linttsc --noEmit -p tsconfig.jsonnpm run build --workspace=packages/backend,nest build)npm test --workspace=packages/backend)npm run migration:runthennpm run test:e2e, Postgres 16)smoke-testST-07 and ST-08 (notification limits). They fail the same way onorigin/main(checked with the same DB), so they are pre-existing and unrelated.drop-billing-schema-migration,proxy-no-plan-gate,proxy,proxy-fallback-auth,routing-flow,multi-key-providersDropBillingSchema1803200000000restoresbilling_email_logs(with its PK and 2 indexes) and the two tenant columns. Re-running drops them again.origin/maincode 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.tsc --noEmitpasse2e note:
--maxWorkers=2cannot be combined with the script's--runInBand(Jest rejects the pair). Running Jest directly with 2 workers makes suites collide, because every suite runsdropSchemaon 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/statusreturns 404, and that the boot works with noSTRIPE_*variable.Notes for follow-ups
crm-metricsspecs still containstripe.comemail fixtures. They will disappear with Remover waitlist, CRM, Gemini Free, free-models, GitHub stars e páginas de erro públicas #15.scoring/specificity-signals.tsand two scoring specs useapi.stripe.com/stripe.comas sample prompt text. These are false positives of the Remover billing, Stripe e limites de plano do backend #14 acceptance grep, not billing code.packages/shared/src/error-taxonomy.tsstill classifies the legacyplan_request_limit_exceededrouting reason, and1798000000000/1800100000000/1800200000000map old rows to it and to M204. Nothing emits it any more. It was kept because the shared taxonomy and the frontend fixtures in refactor(frontend): remove the SaaS layer from the dashboard #51 still reference it, so it can be dropped in a later cleanup.smoke-testST-07/ST-08 fail onmain(notification soft/hard limits). This is worth its own issue.🤖 Generated with Claude Code