DEV Community

Cover image for A pay as you go library for Node.js and Stripe on MongoDb
Zied Hamdi
Zied Hamdi

Posted on • Edited on

A pay as you go library for Node.js and Stripe on MongoDb

UserCredits - Simplify Pay-As-You-Go Features for Your Project

As an entrepreneur working on CvLink, I understand the challenges of adding payment and pay-as-you-go features to a project. It's a time-consuming process, and I wanted to build a library in public, documenting the reasons behind my decisions and giving readers the opportunity to provide feedback early in the process.

The library is hosted on GitHub.

While there are existing solutions like LemonSqueezy for implementing pay-as-you-go features, they can be overly complex for simple needs. You may just want to allow users to track their token consumption and pay for them, either before or after usage, without the complexity and cost of a full-fledged service.

What's Typically Needed

To better understand the scope, let's summarize the essential requirements:

  • Tokens: An abstraction of money to decouple from real currency. For example, users can buy 10 tokens for $10 or 20 tokens for $18.
  • Real-time tracking of token consumption or balance.
  • A way for customers to pay for the tokens they've used or to purchase tokens upfront (depending on your billing model).
  • Currency conversion to display token offers in multiple currencies.
  • The ability to refund customers for unused tokens.

Concepts

Taking a straightforward approach, here are the core concepts:

  • Offers: Represent how many tokens users get for their money. This includes scenarios like one-time offers, monthly subscriptions, yearly subscriptions, and discounts based on subscriptions. Offers can also override others with the same overridingKey.
  • Orders: Represent customers' intentions to purchase an offer. We track their status until the payment is successful.
  • User Credits: Provide information about a user's token balance and consumption.
  • Token Timetable: A detailed log of each token consumption, including the date, time, and the service used.

This seems to cover all the essentials for implementing a pay-as-you-go feature in a startup.

Technologies

Projects differ in their database and payment choices, so we expect the need for adapters on these axes. Therefore, it's crucial to keep concepts separate from implementations.

Testing scenarios for buying, consuming, and checking credit balances should be dissociated from the specific technologies used, making them adaptable to different platforms.

Implementation

Here's how I've implemented these concepts:

Implementation Diagram

Offer

An offer specifies its type (e.g., one-time offer or subscription) and a repetition cycle. The parentOffer property specifies relationships, such as discounts for subscribers. Offers with the same overridingKey value override each other.

Multiple sub-offers can share the same overridingKey, representing different discount levels. For example, a free user could buy 10 AI credits for the regular price of $10, a "Starter" user could get a 30% discount, and a "Early adopter" user could have a 50% discount. These offers all have the same overriding key, e.g., overridingKey="10_AI_Credits":

  1. The $10 offer at the root level (parentOffer=null) for default visibility.
  2. The 30% off offer, a child of the "Starter" offer with a weight=1.
  3. The 50% off offer, a child of the "Early adopter" offer with a weight=2.

This way, an "Early adopter", subscribed as a "Starter", gets better privileges by paying 50% less for AI tokens; while keeping the access rules associated to "Starter" accounts.

export interface IOffer {
  cycle: "once" | "weekly" | "monthly" | "yearly";
  kind: "subscription" | "tokens" | "expertise";
  name: string;
  parentOffer: unknown;
  price: number;
  tokenCount: number;
  weight: number;
}
Enter fullscreen mode Exit fullscreen mode

Order

Orders are tied to users intending to purchase an offer. We track their status until payment success. The status field is duplicated to simplify order reading, and the token count is duplicated for robustness.

export interface OrderStatus {
  date: Date;
  message: string;
  status: "pending" | "paid" | "refused";
}

export interface IOrder<K extends object> {
  history: [OrderStatus];
  offerId: K;
  status: "pending" | "paid" | "refused";
  tokenCount: number;
  userId: K;
}
Enter fullscreen mode Exit fullscreen mode

Token Timetable

Token consumption and additions are logged in the token timetable for users to track their token activity.

export interface ITokenTimetable {
  createdAt: Date;
  tokens: number;
  userId: string;
}
Enter fullscreen mode Exit fullscreen mode

User Credits

User credits summarize all transactions, including subscriptions and token balances. Subscriptions duplicate order status for efficient reading.

export interface ISubscription {
  expires: Date;
  offerId: unknown;
  starts: Date;
  status: "pending" | "paid" | "refused";
}

export interface IUserCredits {
  subscriptions: (unknown extends ISubscription ? unknown : never)[];
  tokens: number;
  userId: unknown;
}
Enter fullscreen mode Exit fullscreen mode

Service

The payment interface serves as a facade to manage these concepts. Customers can create orders, execute payments, and check their token balance.

export interface IPayment<T extends object> {
  createOrder(offerId: unknown, userId: unknown): Promise<IOrder<T>>;
  execute(order: IOrder<T>): Promise<IUserCredits>;
  orderStatusChanged(
    orderId: unknown,
    status: "pending" | "paid" | "refused",
  ): Promise<IOrder<T>>;
  remainingTokens(userId: unknown): Promise<IUserCredits>;
}
Enter fullscreen mode Exit fullscreen mode

Implementation

  • MongoDB was chosen for the initial implementation, but migrating to other databases will be straightforward.
  • The payment part is yet to be implemented, with Stripe as the first choice, but flexibility for other implementations.
  • Screens will be developed using SvelteKit, but the library is designed to be adaptable to React, Angular, and Vue as well.

I welcome early contributors to this project, so feel free to contact me if you'd like to contribute or provide feedback.

Thank you for your interest in the UserCredits library! 🚀

Do your career a big favor. Join DEV. (The website you're on right now)

It takes one minute, it's free, and is worth it for your career.

Get started

Community matters

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay