The Server SDK rules from an in-memory copy of your buyer organizations' entitlements, kept current by a push stream from your application's region. This page covers what that means for running your service: how changes arrive, what still works when the region cannot be reached, and the choices you make for it. The model itself is explained in Where the rules run.
How a change reaches your process
The region announces every change to a buyer organization's entitlements on the push stream, and the client refreshes that organization at once. It does not poll and needs no restart of your process.
| What changed | What reaches your process |
|---|---|
| An organization subscribed, changed plan or had its subscription changed | That organization's new entitlements |
| Reported usage was counted | That organization's new usage, so remaining goes down |
| An allowance was reset at the start of a period | That organization's usage back at zero for the new period |
| You published a new plan version | Nothing yet: existing subscribers stay on their version until their subscription changes |
Each process keeps its own mirror and its own push stream. When you run several replicas, each one loads the entitlements of every buyer organization of your application when it starts, and each one is kept current on its own.
remaining follows reported usage only once the region has counted it: usage is sent in batches, by default every 100 events or every 5 seconds, and the change is pushed back after that. A ruling made in between still shows the earlier figure; A ruling holds nothing covers what to do where that matters.
While the region is unreachable
When the push stream drops, the client moves to DISCONNECTED and keeps the last snapshot. Reads go on from it; writes stop.
| What your code does | While DISCONNECTED |
|---|---|
verifyToken() | Keeps working, from the signing keys already fetched |
A read ruling | Allowed from the last snapshot, with source degraded |
A write ruling | Refused with disconnected_write |
guardWrite() | Throws DisconnectedWriteError |
reportUsage() | Kept: the events wait in the backlog and are delivered first once the client is back online |
reserve(), confirm(), release() | Throw, because they need an answer from the region |
The client reconnects with backoff and, once back online, reloads the whole snapshot and delivers the backlog. Nothing in your code has to change state by hand.
A read allowed while disconnected is allowed even if the organization's plan changed in the meantime, because the change could not reach your process. That is why anything that consumes quota, changes state or costs money must be a write.
Choose read or write for each route
| The route | operation | quantity |
|---|---|---|
| Shows a feature, or a page of data the organization already has | 'read' | 0 |
| Consumes allowance: an export, a generation, an API call you meter | 'write' | What it consumes |
| Creates or changes something the plan limits, such as a project or a seat | 'write' | 1 |
| Costs you money on a third-party service | 'write' | What it consumes |
Choosing read for a route that consumes allowance keeps it available offline, and lets it run while nothing can be counted against the plan in real time. Do it only on purpose.
Starting while the region is unreachable
A process that has never reached the region has nothing to rule from and cannot verify session tokens: every token is refused with not_ready until the client has reached the region once. With the NestJS guards, every guarded route then answers 401.
Rulings made in that state follow presets.cold_start. Writes are always refused with disconnected_write. Reads are refused with cold_start_denied by default; with 'allow', they are allowed with source client_default. Choose 'allow' only for reads that may be served to anyone while your service is cut off.
const shiprev = createClient({
base_url: process.env.SHIPREV_BASE_URL!,
api_key: process.env.SHIPREV_API_KEY!,
presets: { cold_start: 'allow' },
});ShipRevModule starts the client in the background, so your NestJS service boots in this state rather than failing to start.
Keep usage through an outage and a restart
Usage reported while the region is unreachable waits in a backlog. Where the backlog lives decides whether it survives a restart of your process:
backlog.mode | Survives a restart | Choose it when |
|---|---|---|
memory (default) | No | Losing the usage of an outage that coincides with a restart is acceptable |
journal | Yes | The process has a writable local disk |
host_adapter | In your store | You run several replicas or have no durable disk; implement BacklogAdapter |
Stop the client on shutdown with stop(), so buffered usage is sent before the process exits. The backlog reference has the interface and the journal options.
Watch it in production
state() returns the client's state, the revision of the mirror and the time of its last update. Expose it in your health endpoint:
export function health() {
const { state, snapshot_revision, last_sync_at } = shiprev.state();
return { shiprev: state, snapshot_revision, last_sync_at }; // 'LIVE' when all is well
}| State | What to do |
|---|---|
LIVE | Nothing |
DISCONNECTED | Check that the process can reach your application's API address; writes are refused until it can |
COLD_OFFLINE | The process has not loaded anything yet; check the API address and the SDK key |
COLD | The client was not started, or was stopped |
Rehearse an outage
setOnline(false) closes the push stream and holds the client offline until setOnline(true). Use it in a test environment to check that your routes answer as you intend while disconnected; do not call it in production code.
shiprev.setOnline(false);
expect(shiprev.state().state).toBe('DISCONNECTED');
expect(
shiprev.adjudicate({ organization_id: testOrganizationId, capability_key: 'report_exports', quantity: 1, operation: 'write' }).reason,
).toBe('disconnected_write');
shiprev.setOnline(true);