A ruling is the Server SDK's answer to one question: may this buyer organization use this capability, in this quantity, right now. Your backend asks before it does the work and acts on the answer. This page covers what to ask, what comes back and what to do with it, for the Server SDK 0.1.0-beta.1, installed with npm install @shiprev/server-sdk-node@beta.
Ask for a ruling
const ruling = shiprev.adjudicate({
organization_id: claims.organization_id, // from the verified session token only
capability_key: 'report_exports',
quantity: 1,
operation: 'write',
});| Option | What to pass |
|---|---|
organization_id | The buyer organization from the verified session token. Never take it from a header, a parameter or the body: the caller controls those |
capability_key | The key of the capability, as defined in your catalog |
quantity | How much of the allowance the call would use; 1 if omitted. 0 asks only whether the capability is granted |
operation | 'write' for anything that consumes quota, changes state or costs money, otherwise 'read'. Required: a ruling without it is refused with invalid_request |
The operation decides what happens while your application's region is unreachable: reads keep working from the last snapshot, writes are refused. Snapshot, mirror and offline covers how to choose it route by route.
adjudicate() or adjudicateAsync()
Both rule from the mirror, the copy of your buyer organizations' entitlements that the Server SDK keeps in memory. They differ in what happens when the mirror cannot decide on its own.
| Method | Returns | When the mirror cannot decide |
|---|---|---|
adjudicate() | A ruling, synchronously, with no I/O | Refuses: capability_not_granted when the mirror does not know the organization or the capability, quota_exhausted beyond the allowance |
adjudicateAsync() | A promise of a ruling | While online, asks your application's region in those two cases and returns its answer, with source authoritative; without an answer, the refusal stands |
Use adjudicate() where a ruling must never wait on the network, for example inside a loop or a hot path. Use adjudicateAsync() where an organization that has just subscribed, or a call that could be paid for as overage, should get the region's answer. The NestJS capability guard uses adjudicateAsync(). Why the region decides beyond the allowance is explained in Overage policies.
Read the ruling
| Field | How to use it |
|---|---|
allowed | Go ahead or refuse. This is the only field you need to act on |
reason | Present on a refusal; tells you which response to give. See the next section |
remaining | What is left of the allowance in the current period, before this call. It does not subtract the quantity you asked for |
limit | The allowance of the current period |
source | Where the ruling came from: realtime from the mirror while online, authoritative from the region, degraded from the last snapshot while offline, client_default when there was no basis for a ruling |
adjudicated_at | When the ruling was made, in milliseconds since the epoch |
remaining and limit are present when the ruling was made from a capability the mirror or the region holds, and absent on refusals that had no such basis. Every capability a plan grants has a numeric allowance, so when they are present they are numbers. Which refusals carry them is listed in Rulings in the reference.
Log source with your refusals. A refusal with source degraded or client_default says that the process was offline or had no basis for a ruling, not that the customer's plan said no.
Turn a refusal into a response
Each reason calls for a different answer to your caller. The status codes below are a suggestion; ShipRev does not prescribe them.
reason | What it means for your caller | A fitting response |
|---|---|---|
quota_exhausted | The call goes beyond what is left of the allowance and no overage was allowed, or, for a write, usage of the period was left unpaid; see Overage policies | 403, with a way to a larger plan |
capability_not_found | The region confirms the organization's plan does not include the capability | 403, with a way to a plan that includes it |
capability_not_granted | The mirror does not know the organization or the capability; adjudicateAsync() would have asked the region | 403; if it persists, check the capability key and the customer's plan |
disconnected_write | A write while the region is unreachable | 503, and try again later |
cold_start_denied | A read before anything was loaded, with the region unreachable | 503, and try again later |
invalid_request | operation missing or unknown, or quantity negative or not finite | A bug in your code; fix the call |
import type { Ruling } from '@shiprev/server-sdk-node';
/** Maps a refused ruling to the answer your API gives; null when the call may go ahead. */
export function refusal(ruling: Ruling): { status: number; reason: string } | null {
if (ruling.allowed) return null;
switch (ruling.reason) {
case 'disconnected_write':
case 'cold_start_denied':
return { status: 503, reason: ruling.reason };
case 'invalid_request':
throw new Error('Invalid ruling request'); // a bug, not a customer problem
default:
return { status: 403, reason: ruling.reason ?? 'denied' };
}
}The NestJS capability guard does not make this distinction: every refusal, including disconnected_write, becomes a 403. Where you want a different status, rule in the handler yourself; see Protect routes.
A ruling holds nothing
A ruling reads the allowance; it does not take any of it. Two requests ruled at the same moment can both be allowed against the same remaining, and the usage you report afterwards is counted by the region whatever the rulings said. Usage beyond the allowance is then settled as Overage policies describes.
When an overrun must not happen, reserve the amount instead of ruling on it: reserve() takes the amount from the allowance at the region before the work starts, holding credits for any part beyond it that can be paid for as overage, and is refused when the amount cannot be covered. See Reserve, confirm and release.
Check a capability without using it
To show or hide a feature, ask with quantity: 0 and operation: 'read'. The ruling says whether the capability is granted, uses nothing, and keeps working from the last snapshot while the region is unreachable.
const canExport = shiprev.adjudicate({
organization_id: claims.organization_id,
capability_key: 'report_exports',
quantity: 0,
operation: 'read',
}).allowed;