@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).
- 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-clientFor the browser client you also need parse installed (it is a peer
dependency):
npm install parseExports
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>;
}| Method | Cloud function | Purpose |
|---|---|---|
checkSubscription({ appId, email }) | dps_subscriptions_check | Is this email’s subscription active for this app? |
listEvents(filter?) | dps_events_list | List bridged Sellub events, newest first |
getEvent({ objectId }) | dps_events_get | Fetch 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
| Variable | Client | Purpose |
|---|---|---|
NEXT_PUBLIC_DPS_PARSE_URL | browser | Parse server URL (Parse.serverURL) |
NEXT_PUBLIC_DPS_APP_ID | browser | Parse application ID |
NEXT_PUBLIC_DPS_JS_KEY | browser | Public JavaScript key |
DPS_PARSE_URL | server | Parse server URL passed as serverUrl |
DPS_APP_ID | server | Parse application ID |
DPS_MASTER_KEY | server | Master 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.
Related
@duabalabs/sellub-client— creates the payments / subscriptions / invoices that DPS later surfaces here.@duabalabs/dps-cli— the mobile release CLI.- DPS API Reference — the
dps_*cloud functions this client calls.