Refactor subscription backend logic#2426
Conversation
|
Someone is attempting to deploy a commit to the Onlook Team on Vercel. A member of the Team first needs to authorize it. |
| // Here you can decide the logic for the carry-over. | ||
| // Example, you may want to carry over 100% of the credits on the first carry-over, | ||
| // and 50% of the credits on the next carry-overs. | ||
| // const max = rate.carryOverTotal === 0 ? rate.left : rate.left * 0.50; | ||
| const max = rate.left; | ||
|
|
||
| // For now, we only carry over the credits on the first carry-over. | ||
| // In the future, we may want to carry over the credits on the next carry-overs. | ||
| if (rate.carryOverTotal === 0) { |
There was a problem hiding this comment.
This is where you can adjust the carry-over rules. Right now it is set up as:
- It only carries over once.
- 100% of the remaining credits carry over.
| timestamp: new Date(), | ||
| // running a transaction helps with concurrency issues and ensures that | ||
| // the usage is incremented atomically | ||
| return db.transaction(async (tx) => { |
There was a problem hiding this comment.
What happens when increment is called on a free plan with no rateLimits? Seems like in this case there should be an upsert or some logic here to handle that? Seeing a 500 error when running with an existing user with no subscription
There was a problem hiding this comment.
@onlook/web-client dev: [TRPC] usage.increment took 327ms to execute
│ @onlook/web-client dev: ⨯ Error [TRPCError]: Rollback
│ @onlook/web-client dev: at <unknown> (src/server/api/routers/usage/index.ts:122:19)
│ @onlook/web-client dev: at async (src/server/api/trpc.ts:105:19)
│ @onlook/web-client dev: at async streamResponse (src/app/api/chat/route.ts:102:29)
│ @onlook/web-client dev: 120 | // if there are no credits left then rollback
│ @onlook/web-client dev: 121 | if (!limit?.left) {
│ @onlook/web-client dev: > 122 | tx.rollback();
│ @onlook/web-client dev: | ^
│ @onlook/web-client dev: 123 | return;
│ @onlook/web-client dev: 124 | }
│ @onlook/web-client dev: 125 | {
│ @onlook/web-client dev: code: 'INTERNAL_SERVER_ERROR',
│ @onlook/web-client dev: [cause]: Error [DrizzleError]: Rollback
│ @onlook/web-client dev: at <unknown> (src/server/api/routers/usage/index.ts:122:19)
│ @onlook/web-client dev: at async (src/server/api/trpc.ts:105:19)
│ @onlook/web-client dev: at async streamResponse (src/app/api/chat/route.ts:102:29)
│ @onlook/web-client dev: 120 | // if there are no credits left then rollback
│ @onlook/web-client dev: 121 | if (!limit?.left) {
│ @onlook/web-client dev: > 122 | tx.rollback();
│ @onlook/web-client dev: | ^
│ @onlook/web-client dev: 123 | return;
│ @onlook/web-client dev: 124 | }
│ @onlook/web-client dev: 125 | {
│ @onlook/web-client dev: cause: undefined
│ @onlook/web-client dev: }
│ @onlook/web-client dev: }
│ @onlook/web-client dev: ⨯ Error [TRPCError]: Rollback
│ @onlook/web-client dev: at <unknown> (src/server/api/routers/usage/index.ts:122:19)
│ @onlook/web-client dev: at async (src/server/api/trpc.ts:105:19)
│ @onlook/web-client dev: at async streamResponse (src/app/api/chat/route.ts:102:29)
│ @onlook/web-client dev: 120 | // if there are no credits left then rollback
│ @onlook/web-client dev: 121 | if (!limit?.left) {
│ @onlook/web-client dev: > 122 | tx.rollback();
│ @onlook/web-client dev: | ^
│ @onlook/web-client dev: 123 | return;
│ @onlook/web-client dev: 124 | }
│ @onlook/web-client dev: 125 | {
│ @onlook/web-client dev: code: 'INTERNAL_SERVER_ERROR',
│ @onlook/web-client dev: [cause]: Error [DrizzleError]: Rollback
│ @onlook/web-client dev: at <unknown> (src/server/api/routers/usage/index.ts:122:19)
│ @onlook/web-client dev: at async (src/server/api/trpc.ts:105:19)
│ @onlook/web-client dev: at async streamResponse (src/app/api/chat/route.ts:102:29)
│ @onlook/web-client dev: 120 | // if there are no credits left then rollback
│ @onlook/web-client dev: 121 | if (!limit?.left) {
│ @onlook/web-client dev: > 122 | tx.rollback();
│ @onlook/web-client dev: | ^
│ @onlook/web-client dev: 123 | return;
│ @onlook/web-client dev: 124 | }
│ @onlook/web-client dev: 125 | {
│ @onlook/web-client dev: cause: undefined
│ @onlook/web-client dev: }
│ @onlook/web-client dev: }
* Fixes both auth login buttons from showing loading animation when either one is actually loading
* Fix decimal input support, add VH units. * fix: preserve arrow key behavior
| where: and(eq(subscriptions.userId, user.id), eq(subscriptions.status, SubscriptionStatus.ACTIVE)), | ||
| }); | ||
| if (!subscription) { | ||
| return { rateLimitId: undefined, usageRecordId: undefined }; |
There was a problem hiding this comment.
I think in this case we need to check the records for the free user to make sure they don't go over their free tier limit. Would it make sense to create a rate-limit row? Or just use our custom logic and count the usage record?
This would also not count towards free usage limit because it's a no-op. Ideally we still want to make sure this works. You can simulate free user by editing the subscription table row and marking the subscription as cancelled.

There was a problem hiding this comment.
Ah yeah, great catch
I didn't want to have rows for free users since they have daily rates, if you have a million users then each month would add 30 million rows in the table. On the other hand, your comment does highlight an issue with separating systems and for free users, it involves counting rows on a large table (though, now indexed) which is not optimal.
I was trying to keep it simple so the credits stored in the database were tied to the subscription's lifecycle. The alternative would be to inject two rate limits (one daily and one monthly) when a free user sends a message and then when a free user converts to paid, have the daily/monthly rate limits endedAt in the past.
IMO, it's the sort of thing you want to stabilize so it doesn't have to be revisited until months/years from now. By that time, you may revisit how you monetize things and start from scratch.
There was a problem hiding this comment.
Yeah I see the problem with the usage calculation now. Especially when scaled up. Perhaps having a daily usage count system could make sense.
One last wrinkle here is that the daily usage counting is now only monthly limit counting when user doesn't have a subscription. Seems like this would not reset daily so free users will only have 5 free messages per month. Please let me know if this is accurate.
|
|
||
| const insertRateLimit = async (tx: PgTransaction<any, any, any>, item: StripeItem) => { | ||
| console.log(`Inserting rate limit for subscription ${item.subscription.id}`); | ||
| // One month from the start date. NOT SURE IF THIS IS CORRECT. |
There was a problem hiding this comment.
@http-teapot should we use a better end date here?
There was a problem hiding this comment.
If stripeCurrentPeriodEnd is available then I'd use that so you don't have to calculate 31/30 days based on month
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
Description
Related Issues
Type of Change
Testing
Screenshots (if applicable)
Additional Notes
Important
Refactor subscription backend to use Stripe webhooks and implement rate limiting based on subscription status, updating database schema and frontend accordingly.
route.tsandstripe.ts.route.tsandusage/index.ts.route.ts.rateLimitstable inrate-limits.tsto track usage limits per subscription.subscriptionstable insubscription.tsto include period start and end timestamps.usageRecordstable inusage.tsto include indexing for efficient querying.createCustomerfunction infunctions.tsto handle new customer creation.createCheckoutSessionto includestripeCustomerIdinfunctions.ts.functions.ts.This description was created by
for bf4cdb4. You can customize this summary. It will automatically update as commits are pushed.