DPSSDKs@duabalabs/dps-client

@duabalabs/dps-client

Tiny, typed client for the DPS subscription registry — the cross-app, read-only record of who has an active subscription, fed by Sellub → DPS webhooks and consumed by per-app subscription gates. It also exposes the event log that the bridge writes (order placed, state transitions, payment captures).

npm

  • Source: packages/dps-client
  • License: MIT · Current version: 0.3.x
  • Peer dependency: parse (>=4 <6) — only needed for the browser client.
⚠️

Payment / checkout APIs were removed in v0.2.0. createCheckout, verifyPayment, getOrder, listOrders, withTenant and the tenant helpers no longer exist in this package. All money-creation flows now live in @duabalabs/sellub-client. dps-client is now purely the read side: subscription status and bridged events.

Install

npm install @duabalabs/dps-client
# or
pnpm add @duabalabs/dps-client

For the browser client you also need parse installed (it is a peer dependency):

npm install parse

Exports

import {
  createDpsClient,         // browser — uses the Parse JS SDK
  createDpsServerClient,   // server  — uses fetch + master key
  type DpsClient,          // common method surface for both
  type SubscriptionStatus,
  type CheckSubscriptionInput,
  type DpsEvent,
  type ListEventsInput,
  type GetEventInput,
  type ParseLike,
  type ServerClientOptions,
} from '@duabalabs/dps-client';

The DpsClient interface

Both factories return the same DpsClient interface, so callers can be written once and run on either side:

interface DpsClient {
  checkSubscription(input: CheckSubscriptionInput): Promise<SubscriptionStatus>;
  listEvents(input?: ListEventsInput): Promise<DpsEvent[]>;
  getEvent(input: GetEventInput): Promise<DpsEvent | null>;
}
MethodCloud functionPurpose
checkSubscription({ appId, email })dps_subscriptions_checkIs this email’s subscription active for this app?
listEvents(filter?)dps_events_listList bridged Sellub events, newest first
getEvent({ objectId })dps_events_getFetch one bridged event by its DPS object ID

Types

interface SubscriptionStatus {
  active: boolean;
  tier: string | null;
  expiresAt: string | null;   // ISO-8601
  orderId: string | null;
}
 
interface CheckSubscriptionInput {
  appId: string;
  email: string;
}
 
interface ListEventsInput {
  channelToken?: string;       // filter by Vendure channel token
  type?: string;               // e.g. "order.placed"
  vendureOrderId?: string;     // filter by Vendure order ID
  limit?: number;              // default 100
  skip?: number;               // pagination offset
}
 
interface GetEventInput {
  objectId: string;            // DPS event object ID from listEvents
}

A DpsEvent is a normalised record the bridge writes when Sellub forwards a lifecycle event:

interface DpsEvent {
  objectId: string;
  type: string;                // "order.placed", "order.state_transition",
                               // "payment.state_transition", …
  channelToken: string;        // originating Vendure channel token
  vendureOrderId: string;
  vendureOrderCode?: string;
  fromState?: string;          // state-transition events only
  toState?: string;            // state-transition events only
  occurredAt: string;          // ISO-8601, when it happened in Vendure
  externalEventId: string;     // idempotency key from the bridge
  payload: Record<string, unknown>;
  createdAt: string;           // ISO-8601, when DPS stored it
}

createDpsClient — browser

Uses the Parse JS SDK and a public JS key. Suitable for static-export Next.js sites, SPAs and React Native — anywhere you can’t (and shouldn’t) hold a master key.

function createDpsClient(opts: { Parse: ParseLike }): DpsClient;

ParseLike only requires Parse.Cloud.run, so you can pass the real Parse SDK (or a mock):

interface ParseLike {
  Cloud: {
    run: (name: string, params?: Record<string, unknown>) => Promise<unknown>;
  };
}

Wiring it up

// src/lib/dps/client.ts
import Parse from 'parse';
import { createDpsClient, type DpsClient } from '@duabalabs/dps-client';
 
const DPS_PARSE_URL = process.env.NEXT_PUBLIC_DPS_PARSE_URL || '';
const DPS_APP_ID    = process.env.NEXT_PUBLIC_DPS_APP_ID    || '';
const DPS_JS_KEY    = process.env.NEXT_PUBLIC_DPS_JS_KEY    || '';
 
let cached: DpsClient | null = null;
 
export function getDps(): DpsClient {
  if (cached) return cached;
  Parse.initialize(DPS_APP_ID, DPS_JS_KEY || undefined);
  Parse.serverURL = DPS_PARSE_URL;
  cached = createDpsClient({ Parse: Parse as any });
  return cached;
}

Gate access on a subscription

import { getDps } from '@/lib/dps/client';
 
const status = await getDps().checkSubscription({
  appId: 'duabaconnect',
  email: 'user@example.com',
});
 
if (status.active) {
  // grant access; status.tier / status.expiresAt available
} else {
  // send the user to a Sellub checkout (see @duabalabs/sellub-client)
}

createDpsServerClient — server

Uses fetch directly against the Parse /functions/* REST endpoint with a master key. Use this from API routes, trusted backends and CI.

interface ServerClientOptions {
  serverUrl: string;            // e.g. "https://dps.example.com/parse"
  appId: string;
  masterKey: string;
  fetch?: typeof fetch;         // defaults to globalThis.fetch (Node 18+)
}
 
function createDpsServerClient(opts: ServerClientOptions): DpsClient;
import { createDpsServerClient } from '@duabalabs/dps-client';
 
const dps = createDpsServerClient({
  serverUrl: process.env.DPS_PARSE_URL!,   // https://api.duabaforge.com/parse
  appId:     process.env.DPS_APP_ID!,
  masterKey: process.env.DPS_MASTER_KEY!,
});
 
const status = await dps.checkSubscription({
  appId: 'sellub',
  email: 'ops@buyer.com',
});
⚠️

The master key grants full read/write to the Parse backend. Never expose it to a browser bundle or commit it — load it from a server-only secret. For public clients use createDpsClient with a JS key instead.

If globalThis.fetch isn’t available (older Node, custom runtimes), pass one explicitly:

import undici from 'undici';
 
const dps = createDpsServerClient({
  serverUrl: process.env.DPS_PARSE_URL!,
  appId:     process.env.DPS_APP_ID!,
  masterKey: process.env.DPS_MASTER_KEY!,
  fetch:     undici.fetch as typeof fetch,
});

Reading bridged events

// Newest 50 order.placed events for one channel
const events = await dps.listEvents({
  type: 'order.placed',
  channelToken: 'chan_xxx',
  limit: 50,
});
 
// Drill into one event
const detail = await dps.getEvent({ objectId: events[0].objectId });
if (detail) {
  console.log(detail.type, detail.fromState, '→', detail.toState);
}

Errors

The server client throws on a non-2xx response (or a body containing an error field), with a message prefixed @duabalabs/dps-client:. Wrap calls in try/catch:

try {
  const status = await dps.checkSubscription({ appId, email });
} catch (err) {
  console.error('DPS check failed:', (err as Error).message);
}

The browser client surfaces whatever Parse.Cloud.run rejects with — catch Parse errors the same way.

Both clients transparently unwrap the { success, data } envelope that the cloud functions return, so you always receive the typed payload (e.g. SubscriptionStatus), not the wrapper.

Environment variables

VariableClientPurpose
NEXT_PUBLIC_DPS_PARSE_URLbrowserParse server URL (Parse.serverURL)
NEXT_PUBLIC_DPS_APP_IDbrowserParse application ID
NEXT_PUBLIC_DPS_JS_KEYbrowserPublic JavaScript key
DPS_PARSE_URLserverParse server URL passed as serverUrl
DPS_APP_IDserverParse application ID
DPS_MASTER_KEYserverMaster key — server-only secret

Names are conventions used by DPS consumer apps, not values the package reads itself — you pass the resolved values into createDpsClient / createDpsServerClient. Only NEXT_PUBLIC_* values are safe to expose to a browser bundle.