Skip to content

feat(billing): add usage plans, metering, and enforcement - #721

Merged
urjitc merged 23 commits into
mainfrom
feat/plans-usage-and-limits
Aug 3, 2026
Merged

feat(billing): add usage plans, metering, and enforcement#721
urjitc merged 23 commits into
mainfrom
feat/plans-usage-and-limits

Conversation

@urjitc

@urjitc urjitc commented Aug 3, 2026

Copy link
Copy Markdown
Member

Adds usage plans, metering, and enforcement, plus the settings and pricing UI that surfaces them.

What's in it

Plansautumn.config.ts is now the source of truth for the catalog. Free: 500 standard messages, 30 premium, 50 file uploads per month. Pro at $7.99/mo: 3,000 / 400 / 500. A prepaid premium-credits add-on at $8 per 100.

Metering — three metered features tracked through Autumn: standard_messages, premium_messages, file_uploads. Named "file uploads" rather than "documents" because Document is already a workspace item type.

EnforcementcheckWorkspaceAiMessageAccess runs once per turn before the model call. If the chosen tier is spent it falls back to the other tier (premium → auto, standard → claude-sonnet) rather than to a sibling model; only when both are spent does it block. It fails open on any Autumn error, so a billing outage never takes down chat. The pure decision function is in workspace-ai-access.ts with 6 unit tests. Uploads are gated the same way, with a toast on refusal.

DisclosureAiChatAllowanceNotice sits in the composer header and has exactly two states: "will use a cheaper model" and "blocked until ". No low-balance countdown — premium exhaustion degrades rather than walls, so warning about it manufactures dread.

Settings — moved from a page to a dialog driven by ?settings=account|plan, so upgrade prompts anywhere in the app are linkable. The old route redirects.

Model costsmodels.ts weights were buckets (1–4) that didn't match their own stated derivation. Now real multipliers from Vercel AI Gateway prices (auto 1 … gemini-pro 10). Internal only, never rendered.

Telemetry fixes — extraction and intake were minting a new PostHog person per workflow run via distinctId: actorUserId ?? instanceId (247 phantom uploaders vs 25 real). Fixed at the shared capturePostHogServerEvent with processPerson, which covers both call sites. Extraction now reports credits_used, which provider_mode alone couldn't tell you.

Deploy guard — top-level wrangler.jsonc name was thinkex, colliding with the production env. A bare wrangler deploy would have overwritten the live worker with staging R2/Vectorize while keeping production D1. Renamed to thinkex-dev.

Read this before merging

The Autumn production catalog and Stripe are already live. The plans were pushed and payment was verified end to end — previewAttach on pro in production returns subtotal: 7.99, total: 7.99, usd, a real Autumn → Stripe round trip. Nothing is deployed yet, so production currently runs old code that finds no key and bills nothing. Merging and deploying activates billing for everyone immediately, including the free-tier caps.

Production reads AUTUMN_PROD_SECRET_KEY; dev reads AUTUMN_SECRET_KEY. The split is by variable name, not import.meta.env.PROD — Vite sets that for staging builds too, which would send staging traffic into Autumn production.

Known gaps

  • Enforcement logic is unit-tested but the wiring has not been exercised against a genuinely exhausted account. Cheapest check: attach a customised free plan with tiny allowances to one throwaway customer.
  • A reply that fell back to a cheaper model doesn't say so on the message itself — only the composer hints beforehand.
  • The upload-limit toast has no upgrade button yet.
  • A probe_payment_readiness customer is left in Autumn production from the payment check; worth deleting.
  • atmn plans renders Pro as $0.08/month. The stored amount is correct (7.99, display.primary_text: "$7.99") — it's a CLI display bug dividing by 100, in both environments. Reported separately.

The bottom four commits are unrelated

14fd4479 (MIT relicense), 42d826ab (wrangler 4.118.0), 494ccc2c (dep updates), 29d68c42 (security advisories) were already on local main before this work started. They're housekeeping, not part of this change. Happy to split them into their own PR if you'd rather review them separately.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added account settings dialog with account details, plan management, usage meters, and billing actions.
    • Added Free and Pro plans with metered messages, premium models, and file uploads.
    • Added premium model labels, allowance notices, fallback behavior, and reset dates.
    • Added upload-limit checks with clear messaging when allowances are exhausted.
  • Bug Fixes
    • Settings now opens without losing the current page or workspace context.
    • Production and sandbox billing environments are handled separately to prevent misreporting usage.
  • Documentation
    • Clarified environment key configuration for staging and production.

urjitc and others added 15 commits August 2, 2026 19:39
The AGPL text was the stock FSF copy with the author placeholder never
filled in, so the MIT notice names ThinkEx Inc. to match the entity the
footer and the privacy, cookie, and terms pages already use.

Relicensing needs agreement from the outside contributors who hold
copyright in the existing code; this commit only changes the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.116.0 added bundle-size and startup CPU reporting to `wrangler check
startup`; the worker is at 6.72 MB gzip against the 10 MB limit, so the
size readout is worth having before deploys start failing on it.

Ignore *.cpuprofile: the command writes one into the repo root, and
Wrangler also emits one whenever a deploy fails on startup time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Patch and minor bumps across tiptap 3.29.2, ai 7.0.48, drizzle, posthog,
shiki, knip, and the vite-plus toolchain.

Three packages are held back because patches/ carries hand-written diffs
against their dist output: @cloudflare/think, agents, and
@embedpdf/plugin-zoom. Bumping them needs the patches re-authored first.

Pin @modelcontextprotocol/sdk to 1.29.0 rather than tracking 1.30.0:
agents 0.19.0 depends on 1.29.0, and two copies in the tree make the two
McpServer types nominally distinct, which breaks createMcpHandler.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Move the DOMPurify pin to 3.4.12, continuing 99e2e7f: 3.4.11 was the
patched release then and is the vulnerable one now.

Route @esbuild-kit/core-utils to esbuild 0.25.12. drizzle-kit reaches
esbuild only through the deprecated @esbuild-kit packages, which pin
0.18.20, inside the advisory range. Scoped rather than global so nothing
else moves; `drizzle-kit check` still reads drizzle.config.ts.

The other open alerts cleared on their own in the version sweep:
brace-expansion, fast-uri, postcss, and shell-quote all resolve to
patched releases now, and fast-xml-parser was already at 5.10.1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The default config block shared the production worker name while binding the
staging R2 bucket and Vectorize index alongside the production D1. A
`wrangler deploy` without `--env` therefore targeted the live worker and
swapped its storage out from under it: users on the production database,
reading an empty staging bucket.

Rename the default worker so an env-less deploy lands somewhere harmless.
Production and staging blocks are untouched, and the build scripts already
pass CLOUDFLARE_ENV.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps in the extraction and intake events.

LlamaParse credits were parsed out of the job response and persisted into the
projection metadata, but never reached PostHog, so the cost of the pipeline
could only be estimated. provider_mode records the tier we asked for, which
the cost optimizer is free to disagree with per page; credits_used is what was
actually billed.

Both events also fell back to a synthetic distinct id when no actor was
present — the workflow instance for extraction, the request id for intake —
registering a brand new person per run. One month read as 247 uploaders when
25 people had uploaded anything. Add processPerson to the shared capture
helper and set it from whether a real user is attached, so the event is still
recorded but no person is created.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 1-4 cost levels claimed to come from each model's output-token price, but
Haiku and Flash were both marked 1 alongside Luna despite costing 4.2x and
2.5x more on output. Output price alone also understates the models we send
the most tokens to, since it ignores input and cached input entirely.

Price one typical chat step (~15k input at ~65% cache hit, ~500 output)
against the live catalog at ai-gateway.vercel.sh/v1/models and store the real
multiplier rather than a bucket: Luna 1, Flash 3, Haiku 5, Sonnet 5 / Terra /
Gemini Pro 10. These double as credit weights if usage is ever metered.

Internal only — a raw multiplier means nothing to a user who isn't paying per
message, so the picker shows billingTier instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The picker rendered cost as a fourth segment bar identical to Intelligence and
Speed, which flipped polarity silently — more filled meant better for two of
them and worse for the third — and flattened a 10x spread into "4 of 4".

Show the billing tier instead. A raw multiplier is meaningless to someone not
paying per message, whereas Premium maps directly to the allowance a plan
grants. Both tiers render so the row keeps one height while hovering between
models, and the tier sits on its own row rather than beside the name, which
otherwise forced long names to truncate.

The premium badge variant is a neutral fill rather than a hue: every accent in
the palette is either already claimed (amber is warning, blue is info) or too
light for small text, and brand colors aren't defined yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the catalog as code (autumn.config.ts, pushed with the atmn CLI) so plans
live in the repo and the same file targets sandbox and production, rather than
being copied between environments in a dashboard.

Separates the environments. Production and local dev shared one Autumn key, so
dev sessions landed in the same customer list as real users. AUTUMN_SECRET_KEY
is now sandbox-only and AUTUMN_PROD_SECRET_KEY production-only, resolved by one
shared helper so the better-auth plugin and the usage tracker cannot disagree
about which environment they are in. Deliberately not switched on
import.meta.env.PROD: Vite sets that for every build, so staging would report
into production. A deployment missing its key skips tracking, which is
recoverable, instead of writing to the wrong environment, which is not.

Meters file uploads, which were granted but never consumed. Counted when
extraction is requested rather than when bytes land, because extraction is what
costs money and every extraction routes through one caller. Per upload rather
than per page: nobody knows a PDF's page count before uploading, and median
upload is 5 pages against a p90 of 31.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Settings was a route, so opening it from a workspace navigated the user out of
whatever they were doing. It is now a dialog driven by a validated `settings`
search param: a search param rather than a hash because a hash never reaches
the server, so a cold load would render the page and only pop the dialog in
after hydration. /settings stays as a redirect for existing links.

Adds a plan and usage panel reading live balances from Autumn. Usage bars fill
as you spend rather than draining toward zero, and the number counts the same
direction — a gauge emptying toward a limit makes people ration an allowance
they will almost never reach, which suppresses conversion more than it saves.

Rebuilds the account panel on Item rows. The previous read-only text inputs
looked editable and reserved space for fields a guest account does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The landing page and autumn.config.ts both state prices, so they can drift.
Note where the real numbers live so Pro doesn't ship quoting a price nobody
is charged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The gate existed as a stub returning allowed for everyone, so plans were
granted, tracked, and displayed while nothing applied them.

It returns the model the turn should run on rather than a yes/no, so callers
use what they're handed instead of branching on whether a downgrade happened.
An empty tier falls through to the other one: premium spent lands on `auto`,
and `auto` reaches for premium when standard is spent. That second direction
matters more than it looks — `auto` is the model nobody changes, so if it
blocked the moment standard ran out, the default experience would be the first
thing to break while a premium balance sat unused.

Fails open when Autumn is unreachable. Not being able to read a balance must
never stop someone using the product.

The decision is a pure function so the whole matrix is testable without a
network or a customer; the async wrapper only fetches balances. It costs one
check in the common case and a second only once a tier is empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Without this the fallback is silent: someone picks Sonnet, sends, and gets an
Auto answer with no idea why. The server gate is authoritative but only runs
after they've committed to a message.

`check()` reads the cached customer with no network call, which is what makes
it cheap enough to sit above the composer. Advisory only — the client's copy
can be stale and is trivially bypassed.

Two states, and both describe the next message rather than a balance: the tier
is spent so another model will answer, or nothing is left to send with. There
is deliberately no "running low" warning. A count ticking down in the most
looked-at part of the screen is what makes people ration an allowance they will
never reach, and premium running out is not even a wall.

Scoped to the selected model. Telling someone on Auto that their premium
balance is empty is noise about a bucket they aren't spending from.

The picker carries availability in the detail panel rather than on every
premium row: it's one tier-level fact, and the list would state it three times.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Uploads were metered but never checked, so the cap was decorative and the
allowance in settings could only ever count up.

Checked before the presigned URL is issued rather than at completion, so
someone over their cap never transfers bytes we then reject.

No fallback here, unlike chat. The only cheaper path is keeping the free local
parse and skipping the paid one, which returns worse text with nothing marking
it as degraded — and a user cannot tell degraded extraction from a bad product.
Blocking is the honest option.

Fails open when Autumn is unreachable, matching the message gate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pro read "Coming soon" while being purchasable in-app, and both plans described
their limits as "limited" and "more" — which tells a prospect nothing and can't
be compared.

Uses the numbers autumn.config.ts actually grants, in the same order on both
cards so the two are readable side by side. The `ponytail:` note is gone since
the launch it deferred has happened, replaced by one that says where the numbers
have to stay in sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mintlify

mintlify Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
thinkex 🟢 Ready View Preview Aug 3, 2026, 5:19 AM

@cursor

cursor Bot commented Aug 3, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@capy-ai

capy-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Capy auto-review is paused for this organization because the usage-cycle auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit fedc858.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@urjitc, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fe27f8ea-4a7a-4dcb-8df8-97dc544405ec

📥 Commits

Reviewing files that changed from the base of the PR and between cb60e90 and fedc858.

📒 Files selected for processing (15)
  • autumn.config.ts
  • src/features/account/components/PlanBillingSection.tsx
  • src/features/account/use-pro-plan.ts
  • src/features/workspaces/ai/ai-thread.ts
  • src/features/workspaces/ai/use-workspace-ai-allowance.ts
  • src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx
  • src/features/workspaces/components/ai-chat/AiChatModelPicker.tsx
  • src/features/workspaces/extraction/workspace-file-extraction-workflow.ts
  • src/integrations/autumn/client.server.ts
  • src/integrations/autumn/workspace-ai-access.test.ts
  • src/integrations/autumn/workspace-ai-access.ts
  • src/integrations/autumn/workspace-ai-usage.ts
  • src/integrations/autumn/workspace-file-usage.ts
  • src/integrations/posthog/events.ts
  • src/routes/api/v1/workspaces.$workspaceId.file-upload.ts
📝 Walkthrough

Walkthrough

The pull request adds Autumn billing plans, environment-specific secret handling, AI and file-upload metering, account billing settings, usage-limit messaging, and related telemetry updates.

Changes

Autumn billing integration

Layer / File(s) Summary
Billing configuration and environment wiring
.dev.vars.example, .gitignore, LICENSE, autumn.config.ts, docs/ENVIRONMENT.md, package.json, pnpm-workspace.yaml, src/integrations/autumn/*, src/lib/auth.server.ts, src/routes/__root.tsx, worker-configuration.d.ts, wrangler.jsonc
Autumn plans, secret resolution, customer tracking, authentication integration, providers, dependencies, and environment bindings are added or updated.
Account settings dialog flow
src/components/UserProfileDropdown.tsx, src/features/account/components/*, src/routes/_protected.tsx, src/routes/_protected/settings.tsx
Settings now opens as a route-preserving dialog with Account and Plan & usage tabs. The legacy settings route redirects to the dialog.
Workspace AI entitlement flow
src/features/workspaces/ai/*, src/features/workspaces/components/ai-chat/*, src/components/landing/*, src/components/ui/badge.tsx, src/integrations/autumn/workspace-ai-*
AI requests use Autumn tier allowances, fallback models, reset dates, usage tracking, and premium labels. Chat controls display allowance notices and plan links.
Workspace file metering and telemetry
src/integrations/autumn/workspace-file-usage.ts, src/routes/api/v1/workspaces.$workspaceId.file-upload.ts, src/features/workspaces/extraction/*, src/features/workspaces/upload/*, src/integrations/posthog/*
File uploads check Autumn access before session creation and record usage after extraction is queued. Extraction credits and PostHog person processing are recorded explicitly.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AiChatPromptInput
  participant workspaceAiUsage
  participant Autumn
  participant ai-thread
  User->>AiChatPromptInput: select model and submit message
  AiChatPromptInput->>workspaceAiUsage: check tier allowance
  workspaceAiUsage->>Autumn: check entitlement
  Autumn-->>workspaceAiUsage: allowance and reset data
  workspaceAiUsage-->>AiChatPromptInput: allow, fallback, or block
  ai-thread->>workspaceAiUsage: record resolved model usage
  workspaceAiUsage->>Autumn: track usage unit
Loading

Possibly related PRs

Suggested labels: capy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request's primary changes: usage plans, metering, and enforcement.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plans-usage-and-limits

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb60e901ef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +252 to +254
// May differ from what was selected: an empty tier falls through to the
// other one rather than failing the turn.
modelId = access.modelId;

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 Badge Preserve the fallback model across continuations

When the selected tier is exhausted, the initial turn replaces modelId with access.modelId, but a browser/tool continuation re-enters beforeTurn with ctx.continuation === true, initializes modelId from the original request again, and skips this assignment. The resumed request can therefore run a model from the tier that was already denied; reuse the active usage context's resolved model for every continuation of the turn.

Useful? React with 👍 / 👎.

Comment on lines +89 to +90
// Before the presigned URL, so nobody uploads bytes we then reject.
const access = await checkWorkspaceFileUploadAccess({ env, userId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Meter document imports before enforcing the upload cap

This check applies to every validated upload, including the document plans used for CSV, TSV, Markdown, code, and text files, but successful completion only reaches the new trackWorkspaceFileUploadUsage call through requestWorkspaceFileExtraction in the separate file branch. Document imports therefore never decrement file_uploads and can be uploaded indefinitely despite the advertised 50/500-upload limits; either meter those successful imports or exclude them consistently from the cap.

Useful? React with 👍 / 👎.

Comment on lines +36 to +37
if (allowance.willFallBack) {
const fallbackName = getWorkspaceAiChatModelById("auto").name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Name the actual model used for standard-tier fallback

When a standard model's balance is exhausted but premium remains, resolveWorkspaceAiMessageAccess falls back to claude-sonnet, whereas this notice always says Auto will answer. For Auto itself this produces the contradictory message “Auto will answer ... no Auto left” and hides that the turn will consume premium allowance; derive the displayed fallback from the selected tier using the same mapping as the server.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change adds Autumn-backed plans, usage enforcement, billing controls, model-tier fallback, and related telemetry. A focused upload completion test verified that successful document imports create workspace documents without recording file_uploads usage, so the monthly upload limit is not enforced for that import path.

Confidence Score: 4/5

T-Rex T-Rex Logs

What T-Rex did

  • Executed the exact focused Vitest source trex-artifacts/pr-721-document-import-metering-harness.ts.
  • Verified the before-state log shows successful document-import completion without any Autumn metering.
  • Verified the after-state log shows Autumn file-upload metering was invoked for the extraction path.
  • Validated the general contract-validation proof confirms the exact harness was executed and the before/after logs captured the metering behavior.
  • A P1 finding-proof was produced for the posted finding and linked to its review comment.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (2)

  1. src/routes/api/v1/workspaces.$workspaceId.file-upload.ts, line 181-190 (link)

    P1 Document imports bypass upload metering

    The successful document completion branch creates the workspace document and returns without calling requestWorkspaceFileExtraction. That extraction function is currently the only place that emits Autumn's file_uploads usage event, so Markdown and other document-import uploads pass the initial allowance check but never decrement the balance. A user can therefore keep importing documents after reaching the intended monthly upload limit. Meter successful document imports here as well, or move the meter call to a shared, idempotent successful-completion path.

    Artifacts

    Focused document-import and extraction metering harness

    • The exact Vitest harness executed against the completion route and extraction function, showing the asserted document-import bypass and extraction meter call.

    Successful document-import completion without Autumn metering

    • This command output shows the focused Markdown document-import completion test passed while asserting no extraction workflow and no Autumn tracker call, confirming the bypass.

    Extraction path invokes Autumn file-upload metering

    • This command output shows the focused extraction test passed while asserting an Autumn usage call with the file_uploads feature, confirming where metering occurs.

    View artifacts

    T-Rex Ran code and verified through T-Rex

    Fix in Cursor

  2. General comment

    P1 Document-import uploads do not consume the monthly file-upload balance

    • Bug
      • A successful Markdown document-import completion creates a document and returns 200 without calling requestWorkspaceFileExtraction; because the sole Autumn usage call is inside that extraction request, the completed upload does not emit a file_uploads usage event. The initiate endpoint checks the balance before issuing upload access, but successful document imports never decrement it, so repeated imports can continue past the intended monthly cap.
    • Cause
      • The completion handler branches at src/routes/api/v1/workspaces.$workspaceId.file-upload.ts:181-231: the document branch ends after createWorkspaceDocumentFromUpload, whereas only the file branch invokes requestWorkspaceFileExtraction. trackWorkspaceFileUploadUsage is called exclusively by that extraction function at src/features/workspaces/extraction/request-workspace-file-extraction.ts:40-46.
    • Fix
      • Meter successful document imports on completion as well, using the same file_uploads feature and an idempotent usage key/event strategy appropriate for upload completion; alternatively move metering to a common successful-completion point after both document and file branches, preserving the intended failure semantics.

    T-Rex Ran code and verified through T-Rex

Fix All in Cursor

Reviews (1): Last reviewed commit: "feat(pricing): publish Pro at $7.99 with..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/components/landing/PricingSection.tsx (1)

9-36: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Make the pricing limits contract executable.

pricingPlans duplicates the Free and Pro limits from autumn.config.ts. The comment documents drift, but it does not prevent drift. If the catalog changes, the landing page can promise different limits or units than the server enforces. Add a shared public metadata source or a test/build check, and verify that these values represent message counts rather than usage credits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/landing/PricingSection.tsx` around lines 9 - 36, Make the Free
and Pro limits in pricingPlans derive from or be validated against the canonical
autumn.config.ts allowances through a shared public metadata source or
test/build check. Ensure the displayed values are message counts, not
usage-credit amounts, and preserve the existing plan presentation while
preventing future drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/account/components/PlanBillingSection.tsx`:
- Around line 25-26: Update PlanBillingSection to destructure and handle the
useCustomer() error state before deriving or rendering plan details: when error
is present and isLoading is false, render the component’s unavailable/retry
state instead of Free and “Not included.” Guard billing actions, including
attach and openCustomerPortal usage, so they remain disabled or unavailable
until customer exists.

In `@src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx`:
- Around line 36-46: Update the willFallBack notice in AiChatAllowanceNotice to
derive the fallback model using the exported tier-flip logic from
resolveWorkspaceAiMessageAccess in workspace-ai-access.ts, rather than
hardcoding getWorkspaceAiChatModelById("auto"). Ensure the displayed model
matches the backend fallback, including standard-tier exhaustion switching to
claude-sonnet while premium remains.

In `@src/features/workspaces/extraction/workspace-file-extraction-workflow.ts`:
- Around line 151-152: Update the extraction workflow around provider.extract,
writeWorkspacePageProjection, and upsertFileProjection to retain the enhanced
extraction credit metadata before persistence operations begin. In the enclosing
failure path, report the captured credits when extraction completed
successfully, and use creditsUsed: null only when enhanced extraction did not
complete.

In `@src/integrations/autumn/client.ts`:
- Around line 1-10: Rename the Autumn integration module from client.ts to
client.server.ts to enforce the server-only boundary, and update every import of
`#/integrations/autumn/client` to `#/integrations/autumn/client.server`. If client
code requires only the AutumnCustomerFields type, move that type into a
server-independent types-only module and update those type imports accordingly.

In `@src/routes/api/v1/workspaces`.$workspaceId.file-upload.ts:
- Around line 89-102: Make upload admission atomic by replacing the
non-consuming check in the upload route with Autumn’s atomic check-and-consume
call using sendEvent: true, ensuring it runs only once before creating the
direct-upload session. In
src/routes/api/v1/workspaces.$workspaceId.file-upload.ts (89-102), update the
admission flow; in src/integrations/autumn/workspace-file-usage.ts (61-78),
remove or bypass any duplicate upload-allowance consumption; in
src/features/workspaces/extraction/request-workspace-file-extraction.ts (38-46),
retain consumption for extraction completion/failure settlement so queued work
still records its usage.

---

Nitpick comments:
In `@src/components/landing/PricingSection.tsx`:
- Around line 9-36: Make the Free and Pro limits in pricingPlans derive from or
be validated against the canonical autumn.config.ts allowances through a shared
public metadata source or test/build check. Ensure the displayed values are
message counts, not usage-credit amounts, and preserve the existing plan
presentation while preventing future drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 635b4415-2a8f-47be-a628-0b0239faf70f

📥 Commits

Reviewing files that changed from the base of the PR and between 572c019 and cb60e90.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • .dev.vars.example
  • .gitignore
  • LICENSE
  • autumn.config.ts
  • docs/ENVIRONMENT.md
  • package.json
  • pnpm-workspace.yaml
  • src/components/UserProfileDropdown.tsx
  • src/components/landing/PricingSection.tsx
  • src/components/landing/visuals/ModelsVisual.tsx
  • src/components/ui/badge.tsx
  • src/features/account/components/AccountSection.tsx
  • src/features/account/components/AccountSettingsDialog.tsx
  • src/features/account/components/DeleteAccountSection.tsx
  • src/features/account/components/PlanBillingSection.tsx
  • src/features/account/components/SettingsPage.tsx
  • src/features/workspaces/ai/ai-thread.ts
  • src/features/workspaces/ai/models.ts
  • src/features/workspaces/ai/use-workspace-ai-allowance.ts
  • src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx
  • src/features/workspaces/components/ai-chat/AiChatModelPicker.tsx
  • src/features/workspaces/components/ai-chat/AiChatPromptInput.tsx
  • src/features/workspaces/extraction/request-workspace-file-extraction.ts
  • src/features/workspaces/extraction/workspace-file-extraction-observability.ts
  • src/features/workspaces/extraction/workspace-file-extraction-workflow.ts
  • src/features/workspaces/upload/workspace-file-intake-observability.ts
  • src/integrations/autumn/client.ts
  • src/integrations/autumn/secret-key.ts
  • src/integrations/autumn/workspace-ai-access.test.ts
  • src/integrations/autumn/workspace-ai-access.ts
  • src/integrations/autumn/workspace-ai-usage.ts
  • src/integrations/autumn/workspace-file-usage.ts
  • src/integrations/posthog/events.ts
  • src/integrations/posthog/server.ts
  • src/lib/auth.server.ts
  • src/routes/__root.tsx
  • src/routes/_protected.tsx
  • src/routes/_protected/settings.tsx
  • src/routes/api/v1/workspaces.$workspaceId.file-upload.ts
  • worker-configuration.d.ts
  • wrangler.jsonc
💤 Files with no reviewable changes (1)
  • src/features/account/components/SettingsPage.tsx

Comment thread src/features/account/components/PlanBillingSection.tsx Outdated
Comment thread src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx Outdated
Comment thread src/features/workspaces/extraction/workspace-file-extraction-workflow.ts Outdated
Comment on lines +1 to +10
import { Autumn, ResponseValidationError } from "autumn-js";
import { eq } from "drizzle-orm";

import { user } from "#/db/schema";
import { createDbContext } from "#/db/server";
import { resolveAutumnSecretKey } from "#/integrations/autumn/secret-key";
import {
logOperationalEvent,
recordOperationalFailure,
} from "#/integrations/observability/operational-events";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the client/server boundary break: rename this file to a .server.ts module.

CI reports that the staging build fails because client code imports "#/db/schema" through this file. This file imports user from #/db/schema and createDbContext from #/db/server at module scope, then exports functions and types from the same module. Any client-side import of this file — even a type-only import that is not stripped correctly — pulls the DB schema into the client bundle.

src/lib/auth.server.ts already uses the .server.ts suffix for the same reason. Apply the same pattern here: rename client.ts to client.server.ts and update every import path (#/integrations/autumn/client#/integrations/autumn/client.server). If any client component only needs the AutumnCustomerFields type, move that type into a separate types-only module with no server imports so client code doesn't need to reach into the server module at all.

#!/bin/bash
# Find every importer of this module to confirm none run in client-rendered code
# and to enumerate the rename's blast radius.
rg -n --type=ts -C2 "from ['\"]`#/integrations/autumn/client`['\"]"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/integrations/autumn/client.ts` around lines 1 - 10, Rename the Autumn
integration module from client.ts to client.server.ts to enforce the server-only
boundary, and update every import of `#/integrations/autumn/client` to
`#/integrations/autumn/client.server`. If client code requires only the
AutumnCustomerFields type, move that type into a server-independent types-only
module and update those type imports accordingly.

Source: Pipeline failures

Comment on lines +89 to +102
// Before the presigned URL, so nobody uploads bytes we then reject.
const access = await checkWorkspaceFileUploadAccess({ env, userId });

if (!access.allowed) {
return apiError(
requestId,
402,
"upload_limit_reached",
// Gain-framed, and no raw date: the exact reset lives in settings, and a
// server-formatted date has no idea what locale is reading it.
"You've used all your file uploads this month. Pro includes 500 a month.",
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

For autumn-js version 1.2.45, does Autumn provide an atomic check-and-consume, reservation, or idempotent usage operation for feature allowances?

💡 Result:

Yes, for autumn-js, the check operation provides an atomic check-and-consume capability [1][2]. When using the check method, you can pass the parameter sendEvent: true (or send_event: true in the SDK's snake_case convention) [1][3][4]. This combines checking the customer's balance against a required amount with the simultaneous recording (consumption) of that usage in a single atomic operation [2][5]. If the balance is sufficient, the specified amount is deducted immediately as part of that same request [2]. Regarding idempotency, the Autumn API and the track operation support idempotency_key headers and parameters to ensure that events are not double-counted during retries [6][7]. While the check operation's atomic send_event mode is designed for concurrent safety [5], ensure your implementation uses appropriate unique keys if your specific application architecture requires further deduplication guarantees during high-concurrency flows [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Tracked files matching target names:\n'
git ls-files | rg 'workspaces\.\$workspaceId\.file-upload\.ts|workspace-file-usage\.ts|request-workspace-file-extraction\.ts|autumn|package\.json|pnpm-lock\.(yaml|lock)|yarn\.lock|package-lock\.json'

printf '\nRelevant source snippets:\n'
for f in \
  'src/routes/api/v1/workspaces.$workspaceId.file-upload.ts' \
  'src/integrations/autumn/workspace-file-usage.ts' \
  'src/features/workspaces/extraction/request-workspace-file-extraction.ts'
do
  if [ -f "$f" ]; then
    printf '\n--- %s (%s lines) ---\n' "$f" "$(wc -l < "$f")"
    sed -n '1,180p' "$f" | nl -ba
  else
    echo "missing $f"
  fi
done

printf '\nSearch for checkWorkspaceFileUploadAccess and usage consumer functions:\n'
rg -n "checkWorkspaceFileUploadAccess|trackWorkspaceFileUploadUsage|WorkspaceFileUploadAccess|checkWorkspaceFileUploadUsage|requestWorkspaceFileExtraction" src

Repository: ThinkEx-OSS/thinkex

Length of output: 852


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Python implementation of rg-like source scan:\n'
python3 - <<'PY'
from pathlib import Path
import re

target_files = [
    'src/routes/api/v1/workspaces.$workspaceId.file-upload.ts',
    'src/integrations/autumn/workspace-file-usage.ts',
    'src/features/workspaces/extraction/request-workspace-file-extraction.ts',
]
for f in target_files:
    p = Path(f)
    print(f'--- {f} exists={p.exists()} ---')
    if not p.exists():
        continue
    text = p.read_text(encoding='utf-8')
    lines = text.splitlines()
    for i, line in enumerate(lines, 1):
        if 'checkWorkspaceFileUploadAccess' in line or 'trackWorkspaceFileUploadUsage' in line or 'const result = await autumn.check' in line or 'sendEvent' in line or 'send_event' in line or 'reservation' in line:
            start=max(1,i-12); end=min(len(lines), i+25)
            print(f'@@ {f}:{start}-{end} @@')
            for j in range(start,end+1):
                print(f'{j:5d}: {lines[j-1]}')
    print()

print('Autumn check calls with surrounding context:')
for path in Path('src').rglob('*.ts'):
    text = path.read_text(encoding='utf-8')
    if 'autumn.check' in text:
        lines = text.splitlines()
        for i,l in enumerate(lines,1):
            if 'autumn.check' in l:
                for j in range(max(1,i-8), min(len(lines), i+10)+1):
                    print(f'{path}:{j}: {lines[j-1]}')
        print()

print('Search package versions for autumn sdk:')
for p in ['package.json','pnpm-lock.yaml','package-lock.json','yarn.lock']:
    pp=Path(p)
    if not pp.exists(): continue
    txt=pp.read_text(errors='ignore')
    if 'autumn' in txt.lower() or '`@autmn`' in txt:
        print(f'--- {p} ---')
        for line in txt.splitlines():
            if 'autumn' in line.lower() or '`@autmn`' in line:
                print(line)
PY

Repository: ThinkEx-OSS/thinkex

Length of output: 16169


Make upload allowance admission atomic.

Each checkWorkspaceFileUploadAccess call runs before createWorkspaceDirectUploadSession, and check checks the balance without consuming usage. Under concurrency, multiple users can get a presigned upload while one allowance remains, then the separate trackWorkspaceFileUploadUsage call after enqueueing only consumes once. Use Autumn’s atomic check-with-consume path here, such as autumn.check({ ..., sendEvent: true }), and call it only once during upload admission. Keep consumption for extraction usage tied to direct-upload completion/failure handling so queued work still settles when possible.

📍 Affects 3 files
  • src/routes/api/v1/workspaces.$workspaceId.file-upload.ts#L89-L102 (this comment)
  • src/integrations/autumn/workspace-file-usage.ts#L61-L78
  • src/features/workspaces/extraction/request-workspace-file-extraction.ts#L38-L46
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/routes/api/v1/workspaces`.$workspaceId.file-upload.ts around lines 89 -
102, Make upload admission atomic by replacing the non-consuming check in the
upload route with Autumn’s atomic check-and-consume call using sendEvent: true,
ensuring it runs only once before creating the direct-upload session. In
src/routes/api/v1/workspaces.$workspaceId.file-upload.ts (89-102), update the
admission flow; in src/integrations/autumn/workspace-file-usage.ts (61-78),
remove or bypass any duplicate upload-allowance consumption; in
src/features/workspaces/extraction/request-workspace-file-extraction.ts (38-46),
retain consumption for extraction completion/failure settlement so queued work
still records its usage.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

14 issues found across 42 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/workspaces/upload/workspace-file-intake-observability.ts">

<violation number="1" location="src/features/workspaces/upload/workspace-file-intake-observability.ts:74">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The telemetry bug-fix behavior introduced on this line—gating `processPerson` based on `observation.userId` to prevent phantom PostHog persons—is not exercised by any test. Since this is a behavior-change/bug-fix PR and a practical regression assertion exists (mock `capturePostHogServerEvent` and verify `processPerson` is `true` when `userId` is present and `false` when only `requestId` is provided), the rule requires test coverage for the changed behavior.</violation>
</file>

<file name="src/features/workspaces/extraction/workspace-file-extraction-workflow.ts">

<violation number="1" location="src/features/workspaces/extraction/workspace-file-extraction-workflow.ts:275">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `creditsUsed` telemetry reporting is not covered by tests. `getExtractionCreditsUsed` is a pure helper that could regress silently if refactored (e.g., someone changes the `typeof` check), yet no regression-style assertions exercise the three new `creditsUsed` call sites in `recordWorkspaceFileExtractionOutcome`. Since the extraction directory already contains tests for related logic, consider adding a small unit test for the helper and/or verifying the telemetry payload includes the expected `creditsUsed` value after a full extraction run.</violation>
</file>

<file name="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts">

<violation number="1" location="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts:90">
P1: A user at the limit can initiate many uploads concurrently: each request passes this non-reserving check before any completion records usage. Reserve/consume quota atomically when issuing a session (or use a check-and-consume operation), with compensation for expired/failed sessions, so the cap cannot be bypassed by parallel requests.</violation>

<violation number="2" location="src/routes/api/v1/workspaces.$workspaceId.file-upload.ts:92">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new upload-quota enforcement branch that returns `402 upload_limit_reached` is not covered by tests. Since this is a behavior-change PR that activates billing on deploy, the untested guard path creates regression risk. A practical regression test would mock `checkWorkspaceFileUploadAccess` to return `{ allowed: false }` and assert the `402` response with code `upload_limit_reached`, and another to return `{ allowed: true }` and assert the presigned-URL flow continues.</violation>
</file>

<file name="src/integrations/autumn/client.ts">

<violation number="1" location="src/integrations/autumn/client.ts:110">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

The new `trackAutumnUsage` billing integration in `client.ts` is not covered by any test. This is a behavior-change PR adding metering logic, and the rule specifically flags untested changed behavior when regression-style assertions are practical. Consider adding tests that mock the Autumn SDK and assert `getOrCreate`/`track` are called with the correct payloads, cover the `ResponseValidationError` partial-response path, and verify the fail-open operational-logging fallback.</violation>

<violation number="2" location="src/integrations/autumn/client.ts:130">
P2: Concurrent requests near a quota can all pass `check` before background usage events decrement the balance, allowing messages/uploads beyond the allowance. Reserve usage atomically during the access check, then finalize or release that reservation after the model/upload outcome.</violation>
</file>

<file name="src/features/account/components/AccountSettingsDialog.tsx">

<violation number="1" location="src/features/account/components/AccountSettingsDialog.tsx:69">
P3: On phone-sized viewports the settings tabs render as a vertical stack, not the horizontal strip described here, because `orientation` remains `vertical` at every breakpoint. Use responsive orientation/state or override the vertical TabsList/Trigger rules for the mobile layout so the visual layout and keyboard orientation match.</violation>
</file>

<file name="src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx">

<violation number="1" location="src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx:73">
P3: Reset-date formatting now has two identical implementations, so a formatting or timezone correction can diverge between the composer and plan settings. Share one date-formatting utility.</violation>
</file>

<file name="src/routes/__root.tsx">

<violation number="1" location="src/routes/__root.tsx:101">
P2: The client `<AutumnProvider useBetterAuth>` is rendered unconditionally in the root document, but the server-side `autumn()` better-auth plugin it talks to is only mounted when an Autumn secret key resolves (see the conditional spread in createAuth). As a result, in any deployment without a configured Autumn key, the provider will request `/api/auth/autumn`, which doesn't exist in that deployment and returns 404, and the chat-allowance hooks will run against no customer data. Consider gating the provider on the same key-resolution condition (or otherwise detecting that Autumn isn't configured) so keyless environments don't make a guaranteed-failing billing request on every render.</violation>
</file>

<file name="src/features/workspaces/ai/ai-thread.ts">

<violation number="1" location="src/features/workspaces/ai/ai-thread.ts:254">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The enforcement wiring changes in `beforeTurn`—throwing a reset-date-aware error when blocked and silently falling back to `access.modelId` when a tier is exhausted—are behavior changes that are not exercised by any test in the changed file or elsewhere in the repository. While the underlying `resolveWorkspaceAiMessageAccess` logic has unit tests, the integration of that logic into the turn lifecycle (`modelId` reassignment and error formatting) lacks coverage, and the PR itself notes the wiring "has not been exercised against a genuinely exhausted account." Consider adding a regression-style test that mocks `checkWorkspaceAiMessageAccess` and asserts the resulting `TurnConfig` model or the thrown error message.</violation>
</file>

<file name="src/features/account/components/SettingsPage.tsx">

<violation number="1" location="src/features/account/components/SettingsPage.tsx:16">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The settings page migration from a full page to a dialog-driven UI is a meaningful behavior change, yet no test in the PR exercises the claimed redirect from the old route. Under the PR's stated behavior ("old route redirects"), a regression-style test asserting that the former /settings route redirects or opens the appropriate dialog would be practical. Consider adding a routing or redirect test for this migration to prevent silent regressions.</violation>
</file>

<file name="src/features/account/components/AccountSection.tsx">

<violation number="1" location="src/features/account/components/AccountSection.tsx:41">
P3: Accounts without a display name show their email twice: once as the title fallback and again as the description. Render the description only when it differs from the selected display name.</violation>
</file>

<file name="LICENSE">

<violation number="1" location="LICENSE:1">
P2: This commit replaces the project's GNU AGPL v3 (copyleft) with an MIT License as a silent byproduct of a billing/pricing feature PR. Switching license type is a material legal change — it removes the AGPL's network-source-disclosure obligations and relicense of existing AGPL-covered code requires consent from every contributor who authored it — and it has nothing to do with the billing feature set. The PR description itself notes the last four commits are unrelated housekeeping, and this belongs in that category. Consider splitting the license change into its own deliberate commit/review (or dropping it from this PR) so it isn't merged implicitly with feature work.</violation>
</file>

<file name="src/integrations/autumn/workspace-ai-usage.ts">

<violation number="1" location="src/integrations/autumn/workspace-ai-usage.ts:46">
P1: Concurrent turns can exceed a depleted allowance because this check does not reserve a unit and usage is recorded only after each response completes. Use an atomic reservation/consumption step before invoking the model (or serialize allowance consumption per customer) so only one final unit can be admitted.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

* the getOrCreate / track / partial-response / failure handling is identical for
* every metered feature — only the feature id and properties differ.
*/
export async function trackAutumnUsage(input: TrackAutumnUsageInput) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Custom agent: Flag AI Slop and Fabricated Changes

The new trackAutumnUsage billing integration in client.ts is not covered by any test. This is a behavior-change PR adding metering logic, and the rule specifically flags untested changed behavior when regression-style assertions are practical. Consider adding tests that mock the Autumn SDK and assert getOrCreate/track are called with the correct payloads, cover the ResponseValidationError partial-response path, and verify the fail-open operational-logging fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/integrations/autumn/client.ts, line 110:

<comment>The new `trackAutumnUsage` billing integration in `client.ts` is not covered by any test. This is a behavior-change PR adding metering logic, and the rule specifically flags untested changed behavior when regression-style assertions are practical. Consider adding tests that mock the Autumn SDK and assert `getOrCreate`/`track` are called with the correct payloads, cover the `ResponseValidationError` partial-response path, and verify the fail-open operational-logging fallback.</comment>

<file context>
@@ -0,0 +1,148 @@
+ * the getOrCreate / track / partial-response / failure handling is identical for
+ * every metered feature — only the feature id and properties differ.
+ */
+export async function trackAutumnUsage(input: TrackAutumnUsageInput) {
+	const autumn = getAutumnClient(input.env);
+
</file context>

}

// Before the presigned URL, so nobody uploads bytes we then reject.
const access = await checkWorkspaceFileUploadAccess({ env, userId });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: A user at the limit can initiate many uploads concurrently: each request passes this non-reserving check before any completion records usage. Reserve/consume quota atomically when issuing a session (or use a check-and-consume operation), with compensation for expired/failed sessions, so the cap cannot be bypassed by parallel requests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/routes/api/v1/workspaces.$workspaceId.file-upload.ts, line 90:

<comment>A user at the limit can initiate many uploads concurrently: each request passes this non-reserving check before any completion records usage. Reserve/consume quota atomically when issuing a session (or use a check-and-consume operation), with compensation for expired/failed sessions, so the cap cannot be bypassed by parallel requests.</comment>

<file context>
@@ -85,6 +86,20 @@ async function initiateWorkspaceFileUpload(request: Request, workspaceId: string
 		}
 
+		// Before the presigned URL, so nobody uploads bytes we then reject.
+		const access = await checkWorkspaceFileUploadAccess({ env, userId });
+
+		if (!access.allowed) {
</file context>

Comment thread src/features/workspaces/ai/ai-thread.ts
const customerFields = await getAutumnCustomerFields(input.userId);

await autumn.customers.getOrCreate({
const chosen = await autumn.check({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Concurrent turns can exceed a depleted allowance because this check does not reserve a unit and usage is recorded only after each response completes. Use an atomic reservation/consumption step before invoking the model (or serialize allowance consumption per customer) so only one final unit can be admitted.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/integrations/autumn/workspace-ai-usage.ts, line 46:

<comment>Concurrent turns can exceed a depleted allowance because this check does not reserve a unit and usage is recorded only after each response completes. Use an atomic reservation/consumption step before invoking the model (or serialize allowance consumption per customer) so only one final unit can be admitted.</comment>

<file context>
@@ -55,151 +31,72 @@ export interface CheckWorkspaceAiMessageAccessInput {
-		const customerFields = await getAutumnCustomerFields(input.userId);
-
-		await autumn.customers.getOrCreate({
+		const chosen = await autumn.check({
 			customerId: input.userId,
-			...customerFields,
</file context>

Comment thread src/integrations/autumn/workspace-file-usage.ts Outdated
<Tabs
value={tab}
onValueChange={(value) => onTabChange(value as AccountSettingsTab)}
orientation="vertical"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: On phone-sized viewports the settings tabs render as a vertical stack, not the horizontal strip described here, because orientation remains vertical at every breakpoint. Use responsive orientation/state or override the vertical TabsList/Trigger rules for the mobile layout so the visual layout and keyboard orientation match.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/account/components/AccountSettingsDialog.tsx, line 69:

<comment>On phone-sized viewports the settings tabs render as a vertical stack, not the horizontal strip described here, because `orientation` remains `vertical` at every breakpoint. Use responsive orientation/state or override the vertical TabsList/Trigger rules for the mobile layout so the visual layout and keyboard orientation match.</comment>

<file context>
@@ -0,0 +1,117 @@
+				<Tabs
+					value={tab}
+					onValueChange={(value) => onTabChange(value as AccountSettingsTab)}
+					orientation="vertical"
+					className="flex-col gap-0 sm:flex-row"
+				>
</file context>

Comment thread src/features/workspaces/components/ai-chat/AiChatModelPicker.tsx Outdated
);
}

function formatResetDate(resetsAt: number | null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Reset-date formatting now has two identical implementations, so a formatting or timezone correction can diverge between the composer and plan settings. Share one date-formatting utility.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/workspaces/components/ai-chat/AiChatAllowanceNotice.tsx, line 73:

<comment>Reset-date formatting now has two identical implementations, so a formatting or timezone correction can diverge between the composer and plan settings. Share one date-formatting utility.</comment>

<file context>
@@ -0,0 +1,83 @@
+	);
+}
+
+function formatResetDate(resetsAt: number | null) {
+	if (!resetsAt) {
+		return null;
</file context>

Comment thread autumn.config.ts Outdated
Comment thread src/features/workspaces/extraction/workspace-file-extraction-workflow.ts Outdated
The allowance hook imported WORKSPACE_AI_MESSAGE_FEATURE_IDS from
workspace-ai-usage, which pulls in the Autumn client and through it
#/db/schema, so the chat composer dragged the db layer into the browser
bundle and tripped import protection.

Moved the constant to workspace-ai-access, the half with no server-only
imports, which the hook and the usage module both already depend on.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 22 unresolved issues from previous reviews.

Re-trigger cubic

Five separate reports, one shape: the allowance decision was made in one
place and re-derived, differently, somewhere else.

- A continuation re-read the model from the request body, so every tool
  round-trip after the first ran on the tier the gate had just rejected
  and billed it as the model the gate allowed. Reuses the resolved model.
- The composer hardcoded Auto as the fallback, which is right only when
  the empty tier is premium. Running out of standard moves the turn up to
  Claude Sonnet, so the notice named the wrong model and hid that premium
  was about to be spent. Both sides now share one mapping.
- The upload cap covered every upload but only extraction decremented it,
  so document imports were gated and never counted. Extraction is the
  part that costs money, so the gate narrowed to match rather than
  charging for local conversions.
- A failure after extraction reported no credits even though LlamaParse
  had already billed, understating spend on the runs worth investigating.
- An Autumn outage rendered the settings panel as a confident Free with
  nothing included, which is the one wrong answer a paying customer must
  never see.

Also renames client.ts to client.server.ts. Importing it from a component
is what broke the build, and the suffix is how the rest of the repo marks
that boundary.
Markdown, CSV, code, and text imports are converted locally and never
counted against the cap, but the block message read as though every
upload had stopped. Hitting a wall that looks total is what makes people
leave instead of upgrade, and this is the only moment they'd learn
otherwise.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 10 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/integrations/autumn/workspace-ai-access.test.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 19 unresolved issues from previous reviews.

Re-trigger cubic

urjitc added 2 commits August 3, 2026 02:03
The Pro check matched on plan id alone, and the annotation on the callback
hid the rest of the subscription from inference. Autumn returns scheduled
plans in the same array as active ones and the status enum is open, so a
plan that hadn't started could show the Pro badge and replace the upgrade
button with Manage billing.

Unreachable with two plans, where the only scheduled state is a downgrade
to free while pro stays active. Worth pinning before an add-on or a third
plan makes it reachable.
resolveWorkspaceAiMessageAccess delegates to getWorkspaceAiFallbackModelId,
so asserting the two agree compared the function to itself. The two direct
mapping tests are what actually pin the contract.

Also trims two comments that had grown longer than the code they explain.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

0 issues found across 4 files (changes from recent commits).

Requires human review: Auto-approval blocked by 18 unresolved issues from previous reviews.

Re-trigger cubic

urjitc added 3 commits August 3, 2026 02:12
A Pro subscriber who exhausted premium was told to get 400 messages with
Pro, in both the model picker and the composer notice. Subscribers now get
the fact without the pitch, from one shared plan check rather than a third
copy of the subscription matching.

The composer also stops recomputing the allowance decision. It restated the
server's matrix in its own booleans, which is what let it promise one model
while the gate picked another; it now runs resolveWorkspaceAiMessageAccess
and reports the model that came back. That leaves the fallback mapping with
a single caller, so it is no longer exported.

Corrects the catalog comment claiming allowances are metered in credits —
every message and upload decrements by one — and sources the page-count
figures behind per-upload metering.
Upload blocks were already queryable through the rejected intake event, but
an AI block only existed as a thrown error, so counting them meant string
matching "Usage limit reached" inside ai_turn_failed — fragile, and it files
a billing wall next to crashes.

Emitted at the two check functions rather than their callers, so it fires
wherever the gate is consulted, and only when someone is genuinely stopped:
falling back to the other tier is not a wall and should not read as one.

This is the number that says whether the premium top-up in the catalog is
worth wiring into the UI, and for free or paid users.
The fallback model is hardcoded per tier. All three premium models cost the
same today, so the pick is free — but gateway prices move, and if Sonnet
drifts above Terra nothing would notice every fallback landing on the most
expensive model in the tier.

A check rather than logic: deriving the cheapest model would let array order
break a three-way tie, so a catalog reshuffle would silently move the
fallback. Verified it fails by raising Sonnet's cost.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

2 issues found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/account/use-pro-plan.ts">

<violation number="1" location="src/features/account/use-pro-plan.ts:15">
P2: Pro subscribers briefly see upgrade prompts while the customer query loads: this hook returns `false` for both an actual Free customer and unknown/loading state, and the composer/model-picker callers render the pitch for `false`. Expose loading/unknown state (or have callers gate the prompt on `isLoading`) so the prompt waits for a resolved customer.</violation>
</file>

<file name="src/integrations/autumn/workspace-ai-usage.ts">

<violation number="1" location="src/integrations/autumn/workspace-ai-usage.ts:12">
P2: Custom agent: **Flag AI Slop and Fabricated Changes**

The new blocked-path behavior in `checkWorkspaceAiMessageAccess` — firing a `usage_limit_reached` PostHog event when both tiers are exhausted — is not covered by any test in this PR. The 6 existing unit tests only exercise the pure `resolveWorkspaceAiMessageAccess` decision function in `workspace-ai-access.test.ts`, not this wired integration path. A practical regression test could mock `getAutumnClient` and `capturePostHogServerEvent`, exhaust both tiers, and assert the event fires with the expected payload. The PR's own 'Known gaps' section acknowledges this wiring is untested.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

* `subscriptions` also carries scheduled plans, and the status enum is open, so
* only an explicitly active Pro counts.
*/
export function useIsProPlan() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Pro subscribers briefly see upgrade prompts while the customer query loads: this hook returns false for both an actual Free customer and unknown/loading state, and the composer/model-picker callers render the pitch for false. Expose loading/unknown state (or have callers gate the prompt on isLoading) so the prompt waits for a resolved customer.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/account/use-pro-plan.ts, line 15:

<comment>Pro subscribers briefly see upgrade prompts while the customer query loads: this hook returns `false` for both an actual Free customer and unknown/loading state, and the composer/model-picker callers render the pitch for `false`. Expose loading/unknown state (or have callers gate the prompt on `isLoading`) so the prompt waits for a resolved customer.</comment>

<file context>
@@ -0,0 +1,23 @@
+ * `subscriptions` also carries scheduled plans, and the status enum is open, so
+ * only an explicitly active Pro counts.
+ */
+export function useIsProPlan() {
+	const { data: customer } = useCustomer();
+
</file context>

} from "#/integrations/autumn/workspace-ai-access";
import { getAutumnClient, trackAutumnUsage } from "#/integrations/autumn/client.server";
import { recordOperationalFailure } from "#/integrations/observability/operational-events";
import { capturePostHogServerEvent } from "#/integrations/posthog/server";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Custom agent: Flag AI Slop and Fabricated Changes

The new blocked-path behavior in checkWorkspaceAiMessageAccess — firing a usage_limit_reached PostHog event when both tiers are exhausted — is not covered by any test in this PR. The 6 existing unit tests only exercise the pure resolveWorkspaceAiMessageAccess decision function in workspace-ai-access.test.ts, not this wired integration path. A practical regression test could mock getAutumnClient and capturePostHogServerEvent, exhaust both tiers, and assert the event fires with the expected payload. The PR's own 'Known gaps' section acknowledges this wiring is untested.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/integrations/autumn/workspace-ai-usage.ts, line 12:

<comment>The new blocked-path behavior in `checkWorkspaceAiMessageAccess` — firing a `usage_limit_reached` PostHog event when both tiers are exhausted — is not covered by any test in this PR. The 6 existing unit tests only exercise the pure `resolveWorkspaceAiMessageAccess` decision function in `workspace-ai-access.test.ts`, not this wired integration path. A practical regression test could mock `getAutumnClient` and `capturePostHogServerEvent`, exhaust both tiers, and assert the event fires with the expected payload. The PR's own 'Known gaps' section acknowledges this wiring is untested.</comment>

<file context>
@@ -9,6 +9,7 @@ import {
 } from "#/integrations/autumn/workspace-ai-access";
 import { getAutumnClient, trackAutumnUsage } from "#/integrations/autumn/client.server";
 import { recordOperationalFailure } from "#/integrations/observability/operational-events";
+import { capturePostHogServerEvent } from "#/integrations/posthog/server";
 
 export interface TrackWorkspaceAiMessageUsageInput {
</file context>

@urjitc
urjitc merged commit a717001 into main Aug 3, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Dev Board Aug 3, 2026
@urjitc
urjitc deleted the feat/plans-usage-and-limits branch August 3, 2026 06:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant