ShipRev Cloud, step by step

Connect your backend with the Server SDK

Install the Server SDK for Node.js, point it at your application, protect a route and report what it consumed.

Your backend does three things with ShipRev: it verifies the session token of each request, rules on whether the caller's organization may use a capability, and reports what was consumed. All three run inside your process; the network is used only to keep a local mirror of your customers' subscriptions current and to deliver usage. Where the rules run explains the model.

The Server SDK is published for Node.js, with bindings for NestJS. The code below is TypeScript.

1. Install

npm install @shiprev/server-sdk-node@beta

The beta tag installs the current version, 0.1.0-beta.1; new versions are listed in the changelog. The Server SDK is published for Node.js only. Every option and method is listed in the Server SDK reference.

Node.js 20 or later. For the NestJS bindings, your service also needs @nestjs/common and @nestjs/core (9, 10 or 11) and reflect-metadata; a plain Node.js service does not.

2. Configure the values from the console

The application's page in the developer console shows these three lines ready to copy:

.env
SHIPREV_BASE_URL=<the API address of your application>
SHIPREV_API_KEY=<the secret key shown once at creation>
SHIPREV_APP_ID=<the application ID>

SHIPREV_BASE_URL is the application's API address exactly as the console shows it. SHIPREV_API_KEY is the SDK key saved in Application and region. The SDK needs only these two; SHIPREV_APP_ID is the application's ID for your own reference (the key already identifies the application), and you can keep or drop it. Keep the key out of the browser and out of the repository.

3. Start the client

Without NestJS, create and start the client yourself. start() never throws and reports where it ended:

shiprev.ts
import { createClient } from '@shiprev/server-sdk-node';

export const shiprev = createClient({
  base_url: process.env.SHIPREV_BASE_URL!,
  api_key: process.env.SHIPREV_API_KEY!,
});

const { state } = await shiprev.start(); // 'LIVE' once the region answered and the mirror is loaded
// ...
await shiprev.stop(); // sends buffered usage before exiting

If start() does not reach LIVE, check that SHIPREV_BASE_URL is the API address exactly as shown and that the key is the SDK key of this application.

With NestJS, register the module once. It is global, provides one client, and starts it in the background so your service boots even when the region is unreachable:

app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ShipRevModule } from '@shiprev/server-sdk-node/nest';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    ShipRevModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        base_url: config.getOrThrow<string>('SHIPREV_BASE_URL'),
        api_key: config.getOrThrow<string>('SHIPREV_API_KEY'),
      }),
    }),
  ],
})
export class AppModule {}

Stop the client on shutdown so buffered usage is sent; Stop the client on shutdown shows how, with NestJS and without it.

4. Protect routes: token, then capability

With NestJS, apply both guards to the controller and name the capability of each route:

reports.controller.ts
import { Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { type Ruling, ShipRevClient, type TokenClaims } from '@shiprev/server-sdk-node';
import {
  InjectShipRevClient,
  RequireCapability,
  ShipRevAuthGuard,
  ShipRevCapabilityGuard,
} from '@shiprev/server-sdk-node/nest';

/** What the two guards leave on the request once they let it through. */
interface GuardedRequest {
  authClaims: TokenClaims;
  ruling: Ruling;
}

@Controller('api/reports')
@UseGuards(ShipRevAuthGuard, ShipRevCapabilityGuard)
export class ReportsController {
  constructor(@InjectShipRevClient() private readonly shiprev: ShipRevClient) {}

  @Get()
  @RequireCapability('report_exports', 'read', 0)
  list(@Req() request: GuardedRequest) {
    return { organization_id: request.authClaims.organization_id, ruling: request.ruling };
  }

  @Post('export')
  @RequireCapability('report_exports', 'write', 1)
  async export(@Req() request: GuardedRequest) {
    // ... do the work, then report it (step 5)
  }
}

ShipRevAuthGuard answers a missing or invalid session token with 401; ShipRevCapabilityGuard answers a refusal with 403, and the handler is not called.

Without NestJS, do the same two steps yourself: verify the token, then rule with the organization taken from the verified claims.

handler.ts
import { TokenRejectedError } from '@shiprev/server-sdk-node';
import { shiprev } from './shiprev';

export async function exportReport(authorization: string | undefined) {
  const token = authorization?.replace(/^Bearer /, '');
  if (!token) return { status: 401 };

  let claims;
  try {
    claims = await shiprev.verifyToken(token); // signature, expiry, application
  } catch (error) {
    const reason = error instanceof TokenRejectedError ? error.reason : 'invalid_token';
    return { status: 401, reason };
  }

  // The organization comes from the verified token and from nowhere else.
  const ruling = shiprev.adjudicate({
    organization_id: claims.organization_id,
    capability_key: 'report_exports',
    quantity: 1,
    operation: 'write',
  });
  if (!ruling.allowed) return { status: 403, reason: ruling.reason };

  // ... do the work, then report it with the same claims.organization_id (step 5)
  return { status: 200 };
}

Two rules hold for every route, with NestJS or without:

  • Take organization_id only from the verified token, never from a header, a parameter or the body; the caller controls those.
  • Name 'write' for anything that consumes quota, changes state or costs money. Writes are refused while the region is unreachable; reads go on from the last snapshot.

An unannotated route is identity-checked only

The capability guard lets through any route that has no @RequireCapability(). Annotate every route that consumes quota, changes state or costs money.

More on this step: Protect routes for the guards' answers, ruling in the handler and testing that every route is covered; Rulings for what each refusal means; Sessions and tokens for what verification checks and why a token is refused.

5. Report usage

After the work is done, report it with the organization from the same verified token:

ts
import { randomUUID } from 'node:crypto';

await this.shiprev.reportUsage({
  usage_event_id: randomUUID(),
  organization_id: request.authClaims.organization_id,
  capability_key: 'report_exports',
  quantity: 1,
  occurred_at: new Date().toISOString(),
});

Give each piece of work its own usage_event_id; the region drops repeats, so a retried event counts once. Batching, refused events and the backlog are in Report usage; for work whose cost is known only afterwards, see Reserve, confirm and release.

Plan changes reach your process on their own, without polling or a restart; how, and what your service does while the region is unreachable, is in Snapshot, mirror and offline.

Before you deploy

Next

Run a subscription in test mode: define a plan, create a test customer and watch the first usage event arrive.

© 2026 ShipRev