@sftech/nestjs-revenue
v0.1.0
Published
RevenueCat subscription management for NestJS applications. Provides entitlement-based access control, a subscription status endpoint, and a guard/decorator pair for protecting routes behind an active subscription check.
Downloads
96
Readme
@sftech/nestjs-revenue
RevenueCat subscription management for NestJS applications. Provides entitlement-based access control, a subscription status endpoint, and a guard/decorator pair for protecting routes behind an active subscription check.
Installation
pnpm add @sftech/nestjs-revenuePeer Dependencies
| Package | Required Version |
|---|---|
| @nestjs/common | ^10.0.0 \|\| ^11.0.0 |
| @nestjs/core | ^10.0.0 \|\| ^11.0.0 |
| @nestjs/axios | >=4.0.1 |
| @sftech/nestjs-core | >=1.0.3 |
| @sftech/nestjs-auth | >=1.5.0 |
| rxjs | ^7.0.0 |
Note:
@sftech/nestjs-authmust be registered in the consuming application.NestjsRevenueModulerelies on@sftech/nestjs-authfor the@CurrentUser()decorator andIS_PUBLIC_KEYreflection metadata used byEntitlementGuard.
Module Registration
Pass your application configuration object directly to NestjsRevenueModule.register(). The module reads the RevenueCat keys from the configuration and validates required keys at startup.
import { Module } from '@nestjs/common';
import { NestjsAuthModule } from '@sftech/nestjs-auth';
import { NestjsRevenueModule } from '@sftech/nestjs-revenue';
import { Configuration } from '@sftech/nestjs-core';
@Module({
imports: [
NestjsAuthModule.register(Configuration.getConfig()),
NestjsRevenueModule.register(Configuration.getConfig()),
],
})
export class AppModule {}The module is decorated with @Global(), so RevenueService and EntitlementGuard are available application-wide once registered.
Configuration Keys
| Key | Required | Default | Description |
|---|---|---|---|
| REVENUECAT_SECRET_KEY | Yes | - | RevenueCat secret API key (e.g. sk_live_...) |
| REVENUECAT_ENTITLEMENT_ID | Yes | - | Entitlement identifier to check (e.g. premium) |
| REVENUECAT_API_URL | No | https://api.revenuecat.com | RevenueCat REST API base URL |
| REVENUECAT_PLATFORM | No | (none) | Platform sent in X-Platform header (see ERevenuePlatform). When not set, the X-Platform header is omitted entirely. Leave unconfigured when serving multiple platforms (web, Android, iOS) from a single backend — RevenueCat resolves subscribers cross-platform via the app_user_id alone. |
If REVENUECAT_SECRET_KEY or REVENUECAT_ENTITLEMENT_ID is missing, NestjsRevenueModule.register() throws a BaseApplicationException at startup with a descriptive message.
API Reference
Interfaces
| Interface | Description |
|---|---|
| IRevenueConfig | Internal configuration shape used by the module after mapping |
| IRevenuePort | Port interface for the RevenueCat adapter (getSubscriber(appUserId)) |
| ISubscriberStatus | Shape returned by RevenueService and the status endpoint |
IRevenueConfig
interface IRevenueConfig {
secretKey: string;
apiUrl: string;
entitlementId: string;
platform?: ERevenuePlatform;
}IRevenuePort
interface IRevenuePort {
getSubscriber(appUserId: string): Promise<ISubscriberStatus>;
}ISubscriberStatus
interface ISubscriberStatus {
isPremium: boolean;
entitlement: string | null;
expiresAt: Date | null;
activeSubscriptions: string[];
}Enums
ERevenuePlatform
| Member | Value |
|---|---|
| IOS | 'ios' |
| ANDROID | 'android' |
| STRIPE | 'stripe' |
| AMAZON | 'amazon' |
| MACOS | 'macos' |
| PLAY_STORE | 'play_store' |
ERevenueConfigKeys
| Member | Value |
|---|---|
| SECRET_KEY | 'REVENUECAT_SECRET_KEY' |
| API_URL | 'REVENUECAT_API_URL' |
| ENTITLEMENT_ID | 'REVENUECAT_ENTITLEMENT_ID' |
| PLATFORM | 'REVENUECAT_PLATFORM' |
Classes
| Class | Layer | Description |
|---|---|---|
| RevenueService | Application | Facade service; inject this to query subscription status |
| RevenueConfigMapper | Infrastructure | Static mapper that transforms raw config into IRevenueConfig |
| EntitlementGuard | Presentation | NestJS guard that blocks non-premium users with 403 Forbidden |
| SubscriptionController | Presentation | Registers GET /subscription/status endpoint automatically |
Decorator
| Decorator | Description |
|---|---|
| @RequireEntitlement() | Shorthand for @UseGuards(EntitlementGuard) |
REST Endpoint
GET /subscription/status
Returns the authenticated user's subscription status. Authentication is required and handled by @sftech/nestjs-auth. The endpoint is registered automatically when NestjsRevenueModule is imported.
Response (200 OK, active subscription):
{
"isPremium": true,
"entitlement": "premium",
"expiresAt": "2026-12-31T23:59:59.000Z",
"activeSubscriptions": ["monthly_premium"]
}Response (200 OK, no active subscription):
{
"isPremium": false,
"entitlement": null,
"expiresAt": null,
"activeSubscriptions": []
}Response (200 OK, lifetime subscription):
{
"isPremium": true,
"entitlement": "premium",
"expiresAt": null,
"activeSubscriptions": ["lifetime_premium"]
}Error responses:
| Status | Condition |
|---|---|
| 401 Unauthorized | User is not authenticated (handled by @sftech/nestjs-auth) |
Usage Examples
Protecting a Route with EntitlementGuard
import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@sftech/nestjs-auth';
import { EntitlementGuard } from '@sftech/nestjs-revenue';
@Controller('premium-content')
@UseGuards(JwtAuthGuard, EntitlementGuard)
export class PremiumContentController {
@Get()
getContent() {
return { data: 'This is premium content' };
}
}Non-premium users receive 403 Forbidden with the message "Active entitlement required".
Using the @RequireEntitlement() Decorator
@RequireEntitlement() is a convenience wrapper for @UseGuards(EntitlementGuard) and behaves identically:
import { Controller, Get } from '@nestjs/common';
import { JwtAuthGuard, Public } from '@sftech/nestjs-auth';
import { RequireEntitlement } from '@sftech/nestjs-revenue';
@Controller('premium-content')
export class PremiumContentController {
@Get('protected')
@RequireEntitlement()
getProtected() {
return { data: 'Premium only' };
}
@Get('free')
@Public()
getFree() {
return { data: 'Free for everyone' };
}
}Routes decorated with @Public() from @sftech/nestjs-auth bypass EntitlementGuard entirely.
Injecting RevenueService
Use RevenueService directly when you need programmatic access to subscription status:
import { Injectable } from '@nestjs/common';
import { RevenueService, ISubscriberStatus } from '@sftech/nestjs-revenue';
@Injectable()
export class MyFeatureService {
constructor(private readonly revenueService: RevenueService) {}
async getFeatureAccess(userId: string): Promise<ISubscriberStatus> {
return this.revenueService.getSubscriberStatus(userId);
}
async canAccessPremiumFeature(userId: string): Promise<boolean> {
return this.revenueService.hasActiveEntitlement(userId);
}
}Edge Case Behavior
Fail-Open on API Errors
If the RevenueCat API is unreachable, returns a non-200 status, or the response cannot be parsed, the adapter logs a warning and returns a safe non-premium status:
{
"isPremium": false,
"entitlement": null,
"expiresAt": null,
"activeSubscriptions": []
}This means API errors do not cause 500 responses or surface to the end user. The endpoint always returns 200 OK.
Lifetime Subscriptions
When RevenueCat reports expires_date: null for an entitlement or subscription, it indicates a lifetime (non-expiring) purchase. The adapter treats these as always active:
isPremiumistrueexpiresAtisnull(no expiry date)- The subscription key appears in
activeSubscriptions
