Skip to content

FEATURE: Add Autumn subscriptions and usage tracking - #38

Open
bmdavis419 wants to merge 7 commits into
review/hosted-08-abusefrom
review/hosted-09-billing
Open

bmdavis419 wants to merge 7 commits into
review/hosted-08-abusefrom
review/hosted-09-billing

Conversation

@bmdavis419

@bmdavis419 bmdavis419 commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Add Free and Pro subscriptions, checkout, billing management, and local storage/AI limits. Verified Free organizations retain public sharing. Billing controls are owner-only, show pending state, and recover from bounded request failures.

Indexing reserves local AI quota before provider work and commits successful indexing with usage. Queue delivery synchronizes absolute storage and UTC-calendar-month AI balances, with serialized writes and durable recovery. Signed Autumn webhooks reconcile authoritative subscriptions under an organization lock. Real billing keys require a webhook secret.

Billing customers use the stored organization name and a deterministic known owner’s email. An ownerless organization defers customer creation; lookup/provider failures preserve sign-in and retry on a later login.

Validation:

  • Full root suites passed, including 204 route/Postgres cases at this layer; 14 focused sign-in/billing cases and 20 configuration cases passed after follow-up fixes.
  • TypeScript, Effect, Svelte, formatting, and Worker build passed.
  • Second broad Codex review and targeted follow-up commit reviews were clean; independent provider and quota reviews completed.
  • Provider tests use the installed SDK with intercepted HTTP and local Postgres/Worker bindings. Live checkout, Stripe payment, WorkOS sign-in, and deployed queue delivery remain launch checks.

Stack layer 9/11: depends on #37; followed by #39. No merge or deployment.

Note

Add Autumn subscriptions and usage tracking

  • Integrates the Autumn billing provider via a bounded HTTP wrapper (5s timeout, 2MB limit, no retries) and per-secret SDK caching in autumn.ts.
  • Adds Billing and BillingGates services for plan summaries, checkout/portal URLs, and AI quota enforcement during indexing in billing.ts and billing-gates.ts.
  • Tracks absolute storage and monthly AI usage, syncing counters to Autumn through queued jobs and lifecycle recovery tasks via usage.ts.
  • Implements Svix webhook signature verification and a reconciliation endpoint for organization plan updates in svix.ts and +server.ts.
  • Risk: Introduces DB migrations 0010_billing.sql and 0011_usage_delivery.sql that add monthly AI usage columns and a new reservation table to org_usage.

Macroscope summarized 9df460d.

RetriggerConfidence Score: 3/5

Not safe to merge until the outstanding AI quota enforcement issue is fixed.

Fix All in CodexFindings

  1. P1 Security Downgraded quota can be exceeded
Fix with agent prompt
### Issue 1
apps/web/src/lib/server/usage.ts:147-158
A reservation made while an organization is on Pro can be committed after the organization has been downgraded to Free. This code removes the reservation and adds its full value without checking the current plan limit, so a Free organization can retain more than its allowed monthly AI usage. In-flight indexing work can therefore consume AI quota that the downgraded plan no longer permits.

**How this was verified:** A Postgres-backed check reserved 600 operations on Pro, changed the plan to Free, and committed a Free balance of 600 against the 500-operation allowance.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

  • Adds Autumn-backed Free and Pro subscriptions, owner-only checkout and billing management, signed subscription reconciliation, and durable local storage and AI usage enforcement. The outstanding quota-enforcement issue must be addressed before merging.

Reviews (2) · Last reviewed commit: "Use organization owner details when crea..."

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 9 days. After that, they cost $0.25 per reviewed file.

Or wait 33 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used all 6 included reviews currently available. Your 49 included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 6fa0ff34-4100-4a69-97d9-8e9a89decf20

📥 Commits

Reviewing files that changed from the base of the PR and between 1d969eb and 9df460d.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (66)
  • README.md
  • apps/web/.dev.vars.example
  • apps/web/autumn.config.ts
  • apps/web/migrations-pg/0010_billing.sql
  • apps/web/migrations-pg/0011_usage_delivery.sql
  • apps/web/package.json
  • apps/web/src/env.d.ts
  • apps/web/src/lib/dashboard/api.ts
  • apps/web/src/lib/dashboard/parse.ts
  • apps/web/src/lib/server/billing-webhook.pg.test.ts
  • apps/web/src/lib/server/billing-webhook.test.ts
  • apps/web/src/lib/server/billing-webhook.ts
  • apps/web/src/lib/server/config.test.ts
  • apps/web/src/lib/server/config.ts
  • apps/web/src/lib/server/create-local-key.pg.test.ts
  • apps/web/src/lib/server/edge.ts
  • apps/web/src/lib/server/indexing-reconciliation.pg.test.ts
  • apps/web/src/lib/server/indexing-sql.ts
  • apps/web/src/lib/server/jobs/consumer.test.ts
  • apps/web/src/lib/server/jobs/consumer.ts
  • apps/web/src/lib/server/layer.ts
  • apps/web/src/lib/server/plans.test.ts
  • apps/web/src/lib/server/plans.ts
  • apps/web/src/lib/server/request-auth.ts
  • apps/web/src/lib/server/routes/autumn-webhook-body.test.ts
  • apps/web/src/lib/server/routes/billing.test.ts
  • apps/web/src/lib/server/routes/queues.test.ts
  • apps/web/src/lib/server/services/auth-billing-customer.pg.test.ts
  • apps/web/src/lib/server/services/auth-deletion.pg.test.ts
  • apps/web/src/lib/server/services/auth-roles.pg.test.ts
  • apps/web/src/lib/server/services/auth-signin-race.pg.test.ts
  • apps/web/src/lib/server/services/auth.pg.test.ts
  • apps/web/src/lib/server/services/auth.ts
  • apps/web/src/lib/server/services/autumn.test.ts
  • apps/web/src/lib/server/services/autumn.ts
  • apps/web/src/lib/server/services/billing-gates.ts
  • apps/web/src/lib/server/services/billing.pg.test.ts
  • apps/web/src/lib/server/services/billing.ts
  • apps/web/src/lib/server/services/files/internals.ts
  • apps/web/src/lib/server/services/files/purge.ts
  • apps/web/src/lib/server/services/files/thumbnails.ts
  • apps/web/src/lib/server/services/files/upload.ts
  • apps/web/src/lib/server/services/grant-secrets.pg.test.ts
  • apps/web/src/lib/server/services/indexing.ts
  • apps/web/src/lib/server/services/lifecycle.test.ts
  • apps/web/src/lib/server/services/lifecycle.ts
  • apps/web/src/lib/server/services/org.pg.test.ts
  • apps/web/src/lib/server/services/scanner.pg.test.ts
  • apps/web/src/lib/server/services/sites/sessions.ts
  • apps/web/src/lib/server/services/workos.test.ts
  • apps/web/src/lib/server/svix.test.ts
  • apps/web/src/lib/server/svix.ts
  • apps/web/src/lib/server/test/route-context.ts
  • apps/web/src/lib/server/usage.pg.test.ts
  • apps/web/src/lib/server/usage.ts
  • apps/web/src/routes/api/billing/+server.ts
  • apps/web/src/routes/api/billing/checkout/+server.ts
  • apps/web/src/routes/api/billing/portal/+server.ts
  • apps/web/src/routes/api/webhooks/autumn/+server.ts
  • apps/web/src/routes/settings/+page.svelte
  • apps/web/src/routes/settings/billing/+page.svelte
  • apps/web/worker-configuration.d.ts
  • apps/web/wrangler.jsonc
  • docs/billing.md
  • docs/release.md
  • packages/shared/src/index.ts

Comment @coderabbitai help to get the list of available commands.

@bmdavis419
bmdavis419 added this pull request to stack #41 September 11, 2026 04:32
Comment thread apps/web/src/lib/server/services/indexing.ts Outdated
Comment thread apps/web/src/routes/api/webhooks/autumn/+server.ts Outdated

yield* completePurge(sql, org.id, fileId);
forgetTagListCache(org.id);
yield* sendUsageSync;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High files/purge.ts:164

When sendUsageSync fails after completePurge, Autumn is never notified that the file's storage was released, so the org's billing usage remains stale indefinitely if no later mutation occurs. sendUsageSync uses jobs.trySend, which swallows enqueue failures, and the completed purge row is no longer available for sweepPurges to rediscover; use a durable usage-sync reconciliation or propagate and retry the enqueue.

Also found in 2 other location(s)

apps/web/src/lib/server/services/files/internals.ts:118

sendUsageSync uses jobs.trySend, which deliberately swallows a queue-send outage. Unlike indexing, purge, and site cleanup, the lifecycle sweep has no usage-sync reconciliation and there is no persisted usage job state; if this enqueue fails and the org makes no later mutation, its updated storage/AI counters are never sent to Autumn, leaving billing permanently stale.

apps/web/src/lib/server/services/files/upload.ts:113

sendUsageSync uses jobs.trySend, which deliberately logs and suppresses enqueue failures, but there is no periodic reconciliation that enqueues usage-sync from the durable org_usage counters. If the queue is unavailable for an upload (and the org performs no later metered action), its storage balance and pending AI operations are never sent to Autumn; subsequent provider-side AI checks use stale usage and can continue allowing work beyond the plan limit.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/services/files/purge.ts around line 164:

When `sendUsageSync` fails after `completePurge`, Autumn is never notified that the file's storage was released, so the org's billing usage remains stale indefinitely if no later mutation occurs. `sendUsageSync` uses `jobs.trySend`, which swallows enqueue failures, and the completed purge row is no longer available for `sweepPurges` to rediscover; use a durable usage-sync reconciliation or propagate and retry the enqueue.

Also found in 2 other location(s):
- apps/web/src/lib/server/services/files/internals.ts:118 -- `sendUsageSync` uses `jobs.trySend`, which deliberately swallows a queue-send outage. Unlike indexing, purge, and site cleanup, the lifecycle sweep has no usage-sync reconciliation and there is no persisted usage job state; if this enqueue fails and the org makes no later mutation, its updated storage/AI counters are never sent to Autumn, leaving billing permanently stale.
- apps/web/src/lib/server/services/files/upload.ts:113 -- `sendUsageSync` uses `jobs.trySend`, which deliberately logs and suppresses enqueue failures, but there is no periodic reconciliation that enqueues `usage-sync` from the durable `org_usage` counters. If the queue is unavailable for an upload (and the org performs no later metered action), its storage balance and pending AI operations are never sent to Autumn; subsequent provider-side AI checks use stale usage and can continue allowing work beyond the plan limit.

@@ -109,6 +110,7 @@ export const uploadOps = (
forgetTagListCache(org.id);
yield* sendIndexJob(id, 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High files/upload.ts:111

Concurrent sendIndexJob(id, 1) jobs can both pass the AI quota check and embed before either usage is recorded, allowing their combined chunk count to exceed the monthly limit. The quota path performs a read-only Autumn check and only tracks after embedding commits; use Autumn’s atomic check-and-reserve flow before embedding.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/services/files/upload.ts around line 111:

Concurrent `sendIndexJob(id, 1)` jobs can both pass the AI quota check and embed before either usage is recorded, allowing their combined chunk count to exceed the monthly limit. The quota path performs a read-only Autumn `check` and only `track`s after embedding commits; use Autumn’s atomic check-and-reserve flow before embedding.

BillingGates,
Effect.map(AutumnClient, (autumn) =>
BillingGates.of({
canShare: (orgId) =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium services/billing-gates.ts:24

Verified free-plan organizations can publish files or sites publicly, so the public_sharing paid-plan gate is not enforced. canShare is never consumed by the publish authorization path: requirePublishAllowed still checks only canPublish(trust), which explicitly allows every verified organization. Wire canShare into requirePublishAllowed (and deny when it returns false) before permitting publication.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/services/billing-gates.ts around line 24:

Verified free-plan organizations can publish files or sites publicly, so the `public_sharing` paid-plan gate is not enforced. `canShare` is never consumed by the publish authorization path: `requirePublishAllowed` still checks only `canPublish(trust)`, which explicitly allows every verified organization. Wire `canShare` into `requirePublishAllowed` (and deny when it returns `false`) before permitting publication.

forgetTagListCache(org.id);
yield* sendIndexJob(id, 1);
if (visibility.public) yield* sendScanJob(id, 1);
yield* sendUsageSync;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High files/upload.ts:113

Concurrent sendUsageSync jobs can charge the same aiOpsPending value multiple times: syncUsage reads and sends the value to Autumn.track before settling it, so overlapping jobs both report the full amount while the local counter is only reduced to zero afterward. Add per-org serialization or an atomic claim/idempotency key before tracking usage.

Also found in 3 other location(s)

apps/web/src/lib/server/jobs/consumer.ts:111

usageSync has no per-org claim or idempotency key, although every storage mutation and indexing completion can enqueue another identical job. If two queued sync deliveries for the same org overlap, both read the same positive aiOpsPending, both call autumn.track for it, and only afterward subtract it locally; the provider is charged twice for the same embedded chunks.

apps/web/src/lib/server/services/billing.ts:88

syncUsage reads aiOpsPending, sends it to Autumn, and only then subtracts it with a non-atomic update. Two usage-sync jobs for the same org can both read the same positive pending value before either reaches settleAiOps; both calls then invoke autumn.track for that full value, while the two GREATEST(0, ... - value) updates merely leave the local counter at zero. This over-reports AI usage and can overcharge the customer whenever multiple queued syncs overlap.

apps/web/src/lib/server/services/files/thumbnails.ts:121

The new usage-sync enqueue can run concurrently with the other storage/indexing sync jobs for the same org. Each worker's Billing.syncUsage reads the same aiOpsPending, calls Autumn.track for that full value, and only then subtracts it; two workers therefore both report the same AI operations while the local counter is clamped back to zero. A thumbnail upload overlapping another queued sync can consequently overcharge the customer in Autumn.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/services/files/upload.ts around line 113:

Concurrent `sendUsageSync` jobs can charge the same `aiOpsPending` value multiple times: `syncUsage` reads and sends the value to `Autumn.track` before settling it, so overlapping jobs both report the full amount while the local counter is only reduced to zero afterward. Add per-org serialization or an atomic claim/idempotency key before tracking usage.

Also found in 3 other location(s):
- apps/web/src/lib/server/jobs/consumer.ts:111 -- `usageSync` has no per-org claim or idempotency key, although every storage mutation and indexing completion can enqueue another identical job. If two queued sync deliveries for the same org overlap, both read the same positive `aiOpsPending`, both call `autumn.track` for it, and only afterward subtract it locally; the provider is charged twice for the same embedded chunks.
- apps/web/src/lib/server/services/billing.ts:88 -- `syncUsage` reads `aiOpsPending`, sends it to Autumn, and only then subtracts it with a non-atomic update. Two `usage-sync` jobs for the same org can both read the same positive pending value before either reaches `settleAiOps`; both calls then invoke `autumn.track` for that full value, while the two `GREATEST(0, ... - value)` updates merely leave the local counter at zero. This over-reports AI usage and can overcharge the customer whenever multiple queued syncs overlap.
- apps/web/src/lib/server/services/files/thumbnails.ts:121 -- The new `usage-sync` enqueue can run concurrently with the other storage/indexing sync jobs for the same org. Each worker's `Billing.syncUsage` reads the same `aiOpsPending`, calls `Autumn.track` for that full value, and only then subtracts it; two workers therefore both report the same AI operations while the local counter is clamped back to zero. A thumbnail upload overlapping another queued sync can consequently overcharge the customer in Autumn.

siteCleanup: (job) =>
sites.cleanupSession(job.sessionId).pipe(Effect.as('done'))
sites.cleanupSession(job.sessionId).pipe(Effect.as('done')),
usageSync: () => billing.syncUsage.pipe(Effect.as('done'))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High jobs/consumer.ts:111

usageSync redelivers after billing.syncUsage fails, so a successful Autumn.track followed by a failed settleAiOps causes the unchanged aiOpsPending value to be tracked again without an idempotency key. This permanently double-counts usage and can overcharge the organization; make the sync idempotent or prevent redelivery after the provider call succeeds.

Also found in 1 other location(s)

apps/web/src/lib/server/services/files/upload.ts:113

A usage-sync initiated here can be retried after Autumn.track succeeds but before its local pending counter is settled (for example, a transient Postgres failure). The retry reads the unchanged ai_ops_pending and calls track again; Autumn documents that each track records/decrements usage, and this code supplies no idempotency key. This permanently double-charges the same AI operations.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/jobs/consumer.ts around line 111:

`usageSync` redelivers after `billing.syncUsage` fails, so a successful `Autumn.track` followed by a failed `settleAiOps` causes the unchanged `aiOpsPending` value to be tracked again without an idempotency key. This permanently double-counts usage and can overcharge the organization; make the sync idempotent or prevent redelivery after the provider call succeeds.

Also found in 1 other location(s):
- apps/web/src/lib/server/services/files/upload.ts:113 -- A `usage-sync` initiated here can be retried after `Autumn.track` succeeds but before its local pending counter is settled (for example, a transient Postgres failure). The retry reads the unchanged `ai_ops_pending` and calls `track` again; Autumn documents that each `track` records/decrements usage, and this code supplies no idempotency key. This permanently double-charges the same AI operations.

Comment thread apps/web/src/lib/server/billing-webhook.ts Outdated
free: { storedBytes: 2 * GIB },
pro: { storedBytes: 100 * GIB }
free: { storedBytes: 2 * GIB, aiOpsPerMonth: 500 },
pro: { storedBytes: 100 * GIB, aiOpsPerMonth: 10_000 }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium server/plans.ts:8

After a month rollover, syncUsage sends prior-month ai_ops_pending through Autumn.track as new-cycle usage, so delayed operations consume the customer's new-month aiOpsPerMonth allowance while the dashboard reports zero prior usage. Reconcile or clear ai_ops_pending when readOrgUsage advances aiOpsMonth so only current-month operations are tracked.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/plans.ts around line 8:

After a month rollover, `syncUsage` sends prior-month `ai_ops_pending` through `Autumn.track` as new-cycle usage, so delayed operations consume the customer's new-month `aiOpsPerMonth` allowance while the dashboard reports zero prior usage. Reconcile or clear `ai_ops_pending` when `readOrgUsage` advances `aiOpsMonth` so only current-month operations are tracked.

Comment thread apps/web/src/lib/server/config.ts Outdated
Comment thread apps/web/src/lib/server/services/auth.ts Outdated
@bmdavis419
bmdavis419 force-pushed the review/hosted-09-billing branch from 3da061a to a8491a7 Compare September 11, 2026 07:59
@bmdavis419

Copy link
Copy Markdown
Contributor Author

Reviewed the actual Macroscope findings against the revised source. The missing webhook configuration guard was valid and is fixed: real billing keys now require a nonblank webhook secret in development and production; disabled billing and development fakes remain supported. All20 configuration tests passed and targeted independent review is clean.

Other findings are resolved by the reviewed implementation: local quota reservations, bounded raw webhook bodies, durable usage recovery, serialized authoritative subscription reconciliation, absolute current-month balance synchronization, and filtering billing.updated events. Additive track/ai_ops_pending is removed. Free public sharing remains intentional for verified organizations and is represented in the Autumn plan.

These comments originated on the original billing commit even where GitHub now associates their positions with the updated head. Provider sandbox checkout and production verification remain outstanding launch checks.

@bmdavis419
bmdavis419 marked this pull request as ready for review September 11, 2026 08:23
Comment on lines +147 to +158
yield* holdUsage(sql, orgId);
const rows = yield* sql<{ value: number }>`
DELETE FROM ai_usage_reservations
WHERE org_id = ${orgId} AND token = ${token} AND expires_at > clock_timestamp()
RETURNING value`.pipe(Effect.mapError(storage('commit AI reservation')));
const row = rows.at(0);
if (!row)
return yield* new StorageError({
operation: 'commit AI reservation',
cause: 'The AI reservation expired before indexing completed'
});
yield* recordAiOps(sql, orgId, row.value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Downgraded quota can be exceeded

A reservation made while an organization is on Pro can be committed after the organization has been downgraded to Free. This code removes the reservation and adds its full value without checking the current plan limit, so a Free organization can retain more than its allowed monthly AI usage. In-flight indexing work can therefore consume AI quota that the downgraded plan no longer permits.

How this was verified: A Postgres-backed check reserved 600 operations on Pro, changed the plan to Free, and committed a Free balance of 600 against the 500-operation allowance.

Artifacts

Plan-downgrade quota validation source

  • The executed test reserves AI usage on Pro, applies a Free-plan downgrade, and asserts that the committed balance remains within the Free allowance.

Plan-downgrade quota failure

  • The test output shows a Free organization with 600 committed AI operations against a 500-operation allowance, proving the quota can be exceeded.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/src/lib/server/usage.ts
Line: 147-158

Comment:
**Downgraded quota can be exceeded**

A reservation made while an organization is on Pro can be committed after the organization has been downgraded to Free. This code removes the reservation and adds its full value without checking the current plan limit, so a Free organization can retain more than its allowed monthly AI usage. In-flight indexing work can therefore consume AI quota that the downgraded plan no longer permits.

**How this was verified:** A Postgres-backed check reserved 600 operations on Pro, changed the plan to Free, and committed a Free balance of 600 against the 500-operation allowance.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex

bmdavis419 and others added 7 commits September 11, 2026 01:47
autumn.config.ts declares the storage_bytes, ai_ops, and public_sharing
features and the free and pro plans; plans.ts mirrors the limits and a
unit test imports the config to keep them in step. AutumnClient wraps
autumn-js behind a service with a real client when AUTUMN_SECRET_KEY is
set, a fail-open null client that logs once otherwise, and an in-memory
fake for tests. First sign-in creates the Autumn customer for the org.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A usage-sync job reports the org's stored bytes as its storage balance
and tracks the embedded chunks recorded since the last sync; it is sent
after every upload, version commit, site commit, thumbnail, and purge,
and reads the org_usage row when it runs so a burst of sends costs one
report. Migration 0008 adds the monthly and pending AI counters. Before
embedding, indexing asks BillingGates whether the plan allows the
chunks; when it does not, the file finishes keyword-only with
index_error 'AI quota exhausted' and is offered again a day later.
BillingGates.canShare answers the public_sharing feature for the trust
policy to combine with the org's trust. Every gate fails open; the local
org_usage reservation stays the hard stop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/settings/billing shows the org's plan with a storage bar and an AI
operations bar read from org_usage, an Upgrade button that follows the
Autumn checkout URL from POST /api/billing/checkout, and a Manage
billing button that follows the customer portal URL from POST
/api/billing/portal. GET /api/billing serves the summary; the two POSTs
require an owner with a write credential. Without Autumn configured the
buttons are disabled and the summary still renders from local counters.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
POST /api/webhooks/autumn verifies the Svix headers (svix-id,
svix-timestamp, svix-signature) against AUTUMN_WEBHOOK_SECRET with a
WebCrypto HMAC-SHA256 and a five minute timestamp window, logs every
event by type, and on a billing.updated plan list sets orgs.plan to pro
when a pro subscription is in effect and back to free otherwise, so the
quota and trust gates read the plan locally. Unknown customers and other
event types are acknowledged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
const rows = yield* sql<{ id: string }>`
SELECT id FROM orgs WHERE id = ${orgId} FOR UPDATE`;
if (rows.length === 0) return null;
const plan = yield* autumn.getPlan({ customerId: orgId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High server/billing-webhook.ts:42

When a customer is in a Pro trial, billing.updated causes reconcileOrgPlan to write free to orgs.plan, removing the customer's Pro limits and access. autumn.getPlan excludes the documented trialing subscription status, so the reconciliation must treat trialing as an eligible subscription status.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/billing-webhook.ts around line 42:

When a customer is in a Pro trial, `billing.updated` causes `reconcileOrgPlan` to write `free` to `orgs.plan`, removing the customer's Pro limits and access. `autumn.getPlan` excludes the documented `trialing` subscription status, so the reconciliation must treat `trialing` as an eligible subscription status.

Comment on lines +147 to +158
yield* holdUsage(sql, orgId);
const rows = yield* sql<{ value: number }>`
DELETE FROM ai_usage_reservations
WHERE org_id = ${orgId} AND token = ${token} AND expires_at > clock_timestamp()
RETURNING value`.pipe(Effect.mapError(storage('commit AI reservation')));
const row = rows.at(0);
if (!row)
return yield* new StorageError({
operation: 'commit AI reservation',
cause: 'The AI reservation expired before indexing completed'
});
yield* recordAiOps(sql, orgId, row.value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High server/usage.ts:147

commitAiOps records a reservation using the current plan without rechecking its allowance, so work reserved before a downgrade can commit afterward and leave ai_ops_month above the downgraded plan's limit. Compare the reservation plus current usage against planLimits(usage.plan).aiOpsPerMonth and fail the transaction before recording it.

-	yield* holdUsage(sql, orgId);
+	const usage = yield* holdUsage(sql, orgId);
 	const rows = yield* sql<{ value: number }>`
 		DELETE FROM ai_usage_reservations
 		WHERE org_id = ${orgId} AND token = ${token} AND expires_at > clock_timestamp()
 		RETURNING value`.pipe(Effect.mapError(storage('commit AI reservation')));
 	const row = rows.at(0);
 	if (!row)
 		return yield* new StorageError({
 			operation: 'commit AI reservation',
 			cause: 'The AI reservation expired before indexing completed'
 		});
+	if (usage.used + row.value > planLimits(usage.plan).aiOpsPerMonth)
+		return yield* new StorageError({
+			operation: 'commit AI reservation',
+			cause: 'The AI reservation exceeds the current plan allowance'
+		});
 	yield* recordAiOps(sql, orgId, row.value);
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/lib/server/usage.ts around lines 147-158:

`commitAiOps` records a reservation using the current plan without rechecking its allowance, so work reserved before a downgrade can commit afterward and leave `ai_ops_month` above the downgraded plan's limit. Compare the reservation plus current usage against `planLimits(usage.plan).aiOpsPerMonth` and fail the transaction before recording it.

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.

1 participant