## Summary <!-- Provide a short summary of your changes and the motivation behind them. --> ## Related Issues <!-- List any related issues, e.g. Fixes #123 or Closes #456 --> ## Type of Change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Refactor - [ ] Other (please describe): ## Checklist - [ ] I have read the [CONTRIBUTING.md](https://github.com/useautumn/autumn/blob/staging/.github/CONTRIBUTING.md) - [ ] My code follows the code style of this project - [ ] I have added tests where applicable - [ ] I have tested my changes locally - [ ] I have linked relevant issues - [ ] I have added screenshots for UI changes (if applicable) ## Screenshots (if applicable) <!-- Add before/after screenshots or GIFs here --> ## Additional Context <!-- Add any other context or information about the PR here --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Add spend‑limit enforcement to the check API. Checks now respect customer- and entity-level overage limits (including credit-system conversions) with cached and uncached responses matching. - **New Features** - Enforce spend limits via `billing_controls.spend_limits` on customers and entities during checks using a unified `apiSubject`. - Respect credit systems by converting required balance when limits are set on credits features. - Validate entity updates/creates to reject duplicate spend-limit `feature_id`s. - Added integration tests for entity and per-entity products, boundaries, and cache parity. - **Migration** - Replace `EntitySpendLimit` with `DbSpendLimit`; new APIs live under `shared/api/billingControls` (`spendLimit`, `entityBillingControls`). - Use `billing_controls.spend_limits` in payloads and ensure no duplicate `feature_id`s in a single entity update/create. <sup>Written for commit bd1168106e41b12bca265b535f7876298f64a7c2. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> <!-- greptile_comment --> <details><summary><h3>Greptile Summary</h3></summary> This PR wires entity- and customer-level **spend limits** into the `/check` endpoint. When a subject (customer or entity) has an active `spend_limit` for the relevant feature, the available overage is now capped at `overage_limit − current_overage` instead of the balance's own `max_purchase`. The PR also unifies previously fragmented schema files (`EntitySpendLimit` → `DbSpendLimit`, `billingControlModels.ts` → `billingControls/customerBillingControls.ts`) and adds `spend_limits` support at the customer level in addition to entities. **Key changes:** - **[Improvements]** `apiBalanceV1ToAvailableOverage` now resolves the active spend limit via `apiSubjectToSpendLimit` before falling back to `max_purchase`-derived overage — this is the core enforcement logic. - **[Improvements]** `apiEntity` renamed to `apiSubject` throughout the check pipeline (more accurate since it represents either a customer or entity), and added as a field on `CheckData`. - **[API changes]** `CustomerBillingControlsSchema` now includes `spend_limits`, enabling customer-level spend limits alongside per-entity ones. - **[Improvements]** New `ApiEntityBillingControlsInputSchema` with duplicate `feature_id` validation prevents conflicting spend limit entries on create/update. - **[Improvements]** New `apiSubjectToSpendLimit` utility reads spend limits from either a customer or entity API subject in a side-effect-free manner. - **[Bug fixes / Improvements]** `getApiCustomerBase` now populates `billing_controls.spend_limits` from `fullCus.spend_limits` so the customer's spend limits are available in API responses and check logic. - **[Improvements]** Two comprehensive new integration test files covering entity-product and per-entity spend limit enforcement across all billing item types (lifetime, prepaid, consumable, allocated, credit-system), with cached vs. uncached parity assertions. - **[Improvements]** Schema consolidation: `EntitySpendLimit` → `DbSpendLimit`, billing control model files reorganized under `billingControls/`. </details> <h3>Confidence Score: 3/5</h3> - Safe to merge after fixing the cached FullCustomer mutation in fullCustomerToSpendLimit; the core check path is well-tested and correct. - The central spend-limit enforcement logic in `apiBalanceV1ToAvailableOverage` and `apiSubjectToSpendLimit` is sound and backed by thorough integration tests. However, `fullCustomerToSpendLimit` introduces a direct mutation of `fullCustomer.entity` — a field on a potentially shared cached object — which is a data-integrity risk under concurrent load. While the function is currently dead code (imported nowhere), it will cause subtle cache-corruption bugs when wired up. The remaining issues (redundant null guard, early `return` in validation) are minor style points. - Pay close attention to `server/src/internal/balances/autoTopUp/helpers/fullCustomerToSpendLimit.ts` — the side-effecting mutation of a cached object must be resolved before that helper is used. <h3>Important Files Changed</h3> | Filename | Overview | |----------|----------| | server/src/internal/balances/autoTopUp/helpers/fullCustomerToSpendLimit.ts | New helper that extracts a spend limit from a FullCustomer, but mutates `fullCustomer.entity` as a side-effect, which is unsafe when the object is shared from a cache. | | shared/api/customers/cusFeatures/utils/convert/apiBalanceV1ToAvailableOverage.ts | Core spend-limit enforcement: checks for an active spend limit first (overage_limit - current_overage), falling back to max_overage. Logic is correct; contains a redundant null guard on a non-nullable parameter. | | shared/api/billingControls/entityBillingControls.ts | New file providing entity billing controls schema with duplicate feature_id validation; early `return` in superRefine stops reporting after the first duplicate pair. | | shared/api/customers/utils/apiSubjectToSpendLimit.ts | New utility that safely looks up an enabled spend limit for a feature from either a customer or entity API subject; read-only with no side-effects. | | shared/api/customers/cusFeatures/utils/convert/apiBalanceToAllowed.ts | Passes the new `apiSubject` to `apiBalanceV1ToAvailableOverage` so spend limit enforcement participates in the "is this balance allowed" determination. | | shared/models/cusModels/billingControls/customerBillingControls.ts | Adds `spend_limits` to `CustomerBillingControlsSchema`, enabling customer-level spend limits; renamed/relocated from `billingControlModels.ts`. | | server/tests/integration/balances/check/spend-limit/check-entity-product-spend-limit.test.ts | Comprehensive new integration tests covering lifetime, prepaid, consumable, allocated, and credit-system entity product spend limit enforcement with cache-parity assertions. | | server/tests/integration/balances/check/spend-limit/check-per-entity-spend-limit.test.ts | Comprehensive new integration tests for per-entity spend limits across product types, mirroring the entity-product test file with per-entity billing item configs. | </details> <details><summary><h3>Sequence Diagram</h3></summary> ```mermaid sequenceDiagram participant Client participant getCheckData participant getV2CheckResponse participant apiBalanceToAllowed participant apiBalanceV1ToAvailableOverage participant apiSubjectToSpendLimit Client->>getCheckData: check(customer_id, entity_id, feature_id, required_balance) getCheckData->>getCheckData: resolve apiSubject (customer or entity) getCheckData-->>getV2CheckResponse: CheckData { apiSubject, apiBalance, featureToUse, ... } getV2CheckResponse->>apiBalanceToAllowed: { apiBalance, apiSubject, feature, requiredBalance } apiBalanceToAllowed->>apiBalanceV1ToAvailableOverage: { apiBalance, apiSubject, feature } apiBalanceV1ToAvailableOverage->>apiSubjectToSpendLimit: { subject: apiSubject, feature } apiSubjectToSpendLimit-->>apiBalanceV1ToAvailableOverage: spendLimit (or undefined) alt spend limit active apiBalanceV1ToAvailableOverage-->>apiBalanceToAllowed: max(0, spendLimit.overage_limit - current_overage) else no spend limit apiBalanceV1ToAvailableOverage-->>apiBalanceToAllowed: max(0, maxOverage - current_overage) end apiBalanceToAllowed-->>getV2CheckResponse: allowed (boolean) getV2CheckResponse-->>Client: CheckResponseV3 { allowed, balance, ... } ``` </details> <sub>Last reviewed commit: bd11681</sub> > Greptile also left **3 inline comments** on this PR. <!-- /greptile_comment -->
Autumn
Autumn is an open-source layer between Stripe and your application, allowing you to create any pricing model and embed it with a couple lines of code. On Autumn you can build:
- Subscriptions
- Credit systems & top ups
- Usage-based models & overages
- Custom plans for large customers
All this without having to handle webhooks, upgrades/downgrades, cancellations or payment fails.
Getting Started
Cloud: The quickest way to start using Autumn is through our cloud service.
Self Hosted: If you'd like to self-host Autumn:
- Make sure you have
buninstalled - Install the project dependencies
bun install
- Run our set up script:
bun setup
- Generate the relevant tables in your postgres DB
bun db:generate && bun db:migrate
- Run Autumn:
For Windows
docker compose -f docker-compose.dev.yml up
For mac/linux:
docker compose -f docker-compose.unix.yml up
That's it! You should be able to see the Autumn dashboard on http://localhost:3000.
⚠️ To log in, enter an email at the sign in page, and an OTP should appear in your console / terminal. Normally, we use Resend to email an OTP or Google OAuth -- these can be set up by providing your credentials in
server/.env
ℹ️ Our set up script initializes the required env vars and (optionally) a Supabase instance. If you'd like to use your own Postgres instance, you can do so -- just paste the connection string in the
DATABASE_URLenv variable atserver/.env
Troubleshooting
If you encounter a SyntaxError: Unexpected end of JSON input error when running bun setup again after previously running it, you may need to clear your database tables first. This is a known issue that can occur when running database migrations multiple times.
To resolve this:
- Connect to your database
- Drop all existing tables
- Run the setup script again:
Why Autumn
1️⃣ Billing infra gets complex fast
More than payments: it's building permission management, metering, usage limits with cron jobs, and connecting it to upgrade, downgrade, cancellation and failed payments states. Race conditions, edge cases, and other bugs will slow you down.
2️⃣ Billing and app logic should be decoupled
Growing companies iterate on pricing often: raising prices, experimenting with credits or charging for a new feature. DB migrations, rebuilding in-app flows, internal dashboards for custom pricing and grandfathering users on old pricing is a nightmare.
How it works
First, create your products and plans on the dashboard. We support any pricing model. Some popular ones we've seen include:
- Usage & Overage ⚡: set real-time usage limits and choose when they reset. Charge users if they go over.
- Credits 💰: users can access monetary or arbitrary credits that many features can draw from
- Seat-based with per-seat limits 👥:: bill customers for their users (or other entities)
- Pay upfront 💳: let users purchase a fixed quantity of a feature upfront, which is used over time
Next, all your billing logic can be implemented through just 3 functions:
/attach: One function call for all purchase flows. We return a Stripe Checkout URL, or handle an upgrade/downgrade.
const { attach } = useAutumn();
<button
onClick={async () => {
await attach({ productId: "pro" });
}}
>
Upgrade to Pro
</button>
/check: Check whether a customer has access to a product, feature or remaining usage.
const { check } = useAutumn();
const { data } = await check({ featureId: "ai_tokens" })
!data.allowed && alert("AI limit reached")
/track: When a customer uses a usage-based feature, record a usage event.
const { track } = useAutumn();
await track({
featureId: "ai_tokens",
value: 1312
})
Others
Contributing 🤝: If you're interested in contributing, you can check out our guide here. All types of help are appreciated :)
Support 💬: If you need any type of support, we're typically most responsive on our Discord channel, but feel free to email us hey@useautumn.com too!
Contributors
Thanks to all our contributors for helping make autumn a better product!
