@explorins/pers-sdk
v2.1.11
Published
Platform-agnostic SDK for PERS (Phygital Experience Rewards System) - Core business logic and API integration
Readme
@explorins/pers-sdk
Platform-agnostic TypeScript SDK for PERS (Phygital Experience Rewards System) - Modern Manager-Service architecture providing clean, consistent APIs for tourism loyalty applications.
Table of Contents
- @explorins/pers-sdk
- Table of Contents
- Overview
- Dependencies
- Installation
- Quick Start
- Platform Integration
- Architecture
- Core Features
- Configuration
- Advanced Authentication
- Error Handling
- Bundle Size
- TypeScript Types
- Migration Guide
- Documentation
- Related Packages
- License
Overview
PERS (Phygital Experience Rewards System) is a comprehensive tourism loyalty platform bridging physical and digital experiences. It provides:
- Loyalty Programs: Token-based rewards, credit systems, and user tiers
- Campaign Management: Marketing campaigns with automated rewards distribution
- Business Network: Multi-tenant business discovery and partnerships
- Blockchain Integration: EVM-compatible chains (Ethereum, Polygon, Camino)
- Redemption Engine: Reward fulfillment with optional blockchain signing
- Analytics: Real-time transaction and user engagement metrics
Dependencies
Required Peer Dependencies
These packages MUST be installed alongside @explorins/pers-sdk:
npm install @explorins/pers-shared ethers@^6.15.0| Package | Version | Purpose |
|---------|---------|---------|
| @explorins/pers-shared | * | Shared types, interfaces, and DTOs used across all SDK domains |
| ethers | ^6.15.0 | Blockchain/Web3 operations (required for sdk.web3 manager) |
Optional Dependencies (Platform-Specific)
| Package | Required For | Notes |
|---------|--------------|-------|
| rxjs | AngularHttpClientAdapter only | Loaded dynamically at runtime; NOT bundled into SDK |
AI Agent Note: The SDK has ZERO runtime RxJS dependency unless you specifically use
AngularHttpClientAdapter. Browser, Node.js, and React Native integrations do NOT require RxJS.
Direct Dependencies
The SDK bundles essential dependencies for seamless installation:
| Package | Type | Purpose |
|---------|------|---------|
| @explorins/pers-shared | Dependency (bundled) | Shared types, interfaces, and DTOs |
| @explorins/web3-ts | Dependency (bundled) | Web3/blockchain utilities - automatically tree-shaken when not used |
| ethers | Dependency (bundled) | Blockchain operations - automatically tree-shaken when not used |
Tree-Shaking Magic: Web3 features (~500KB install size) are only included in your final bundle if you actually use them. The SDK uses subpath exports (
@explorins/pers-sdk/web3) that modern bundlers (Vite, Webpack 5+, Angular) automatically tree-shake when unused.Note:
npm installdownloads ~500KB of web3 dependencies, but your production bundle will be ~3KB if you don't import web3 features. This design prioritizes developer experience (no peer dependency hassles) while maintaining optimal production bundle sizes through automatic tree-shaking.
Installation
# Single command installs everything - no peer dependency hassle
npm install @explorins/pers-sdk
# Optional: For Angular applications only
npm install rxjsBundle Sizes (After Tree-Shaking):
| Usage Pattern | Install Size | Runtime Bundle Size | Notes | |--------------|--------------|---------------------|-------| | Core SDK only (no web3) | ~500 KB | ~3 KB (ESM) | Campaigns, users, tokens, auth | | With Web3 features | ~500 KB | ~26 KB (ESM) | Includes blockchain operations | | Development (node_modules) | ~500 KB | N/A | All features available for development |
Key Insight: The install downloads 500KB, but your production app only includes what you import. If you never
import { Web3Manager }, those 500KB are automatically excluded from your bundle by the bundler.
How Tree-Shaking Works:
// ✅ This code = ~3 KB production bundle (web3 dependencies NOT included)
import { createPersSDK } from '@explorins/pers-sdk';
const sdk = createPersSDK({...});
await sdk.campaign.list();
// ✅ This code = ~26 KB production bundle (web3 loaded only when imported)
import { Web3Manager } from '@explorins/pers-sdk/web3';
const web3 = new Web3Manager(sdk.api());
await web3.getTokenBalance({...});Modern bundlers (Vite, Webpack 5+, Angular, Rollup) detect when web3 code is never imported and completely remove it from your production bundle through dead code elimination.
Quick Start
Browser / React / Vue
import { createPersSDK } from '@explorins/pers-sdk';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
// Initialize SDK
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
apiProjectKey: 'your-project-key'
});
// Authenticate with external JWT (Firebase, Auth0, Cognito, etc.)
const externalJWT = await yourAuthProvider.getIdToken();
const authResult = await sdk.auth.loginWithToken(externalJWT, 'user');
// Use SDK managers
const campaigns = await sdk.campaigns.getActiveCampaigns();Node.js (Convenience Function)
import { createNodeSDK } from '@explorins/pers-sdk/node';
// One-liner setup with static JWT
const sdk = createNodeSDK({
jwt: 'your-system-jwt',
projectKey: 'your-project-key',
environment: 'production'
});
// Ready to use - no additional auth needed
const campaigns = await sdk.campaigns.getCampaigns();
const userClaims = await sdk.campaigns.getCampaignClaims({ userId: 'user-123' });Complete Example (Node.js Server-Side)
import { createNodeSDK } from '@explorins/pers-sdk/node';
// Initialize with static JWT (tenant/business system token)
const sdk = createNodeSDK({
jwt: process.env.PERS_JWT_TOKEN,
projectKey: process.env.PERS_PROJECT_KEY,
environment: 'production'
});
// Campaign operations (admin/system access)
const campaigns = await sdk.campaigns.getCampaigns();
const campaign = await sdk.campaigns.getCampaignById('campaign-123');
// Claim campaign for a user (system operation)
const claim = await sdk.campaigns.claimCampaign({
campaignId: 'campaign-123',
userIdentifier: 'external-user-id'
});
// Get user's claim history
const userId = claim.user?.id;
const userClaims = await sdk.campaigns.getCampaignClaims({ userId });
// Business operations
const businesses = await sdk.businesses.getActiveBusinesses();Platform Integration
Browser / React / Vue
No additional dependencies required.
import { createPersSDK } from '@explorins/pers-sdk';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
environment: 'production',
apiProjectKey: 'your-project-key'
});
// Ready to use
const campaigns = await sdk.campaigns.getActiveCampaigns();⚠️ Browser Note: Always use
BrowserFetchClientAdapterfor browser/React/Vue apps. Do not importNodeHttpClientAdapterin browser code - it contains Node.js-specific imports that will cause build errors.
Angular
Requires rxjs peer dependency.
npm install rxjsimport { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { createPersSDK, PersSDK } from '@explorins/pers-sdk';
import { AngularHttpClientAdapter } from '@explorins/pers-sdk/platform-adapters';
import { IndexedDBTokenStorage } from '@explorins/pers-sdk/core';
@Injectable({ providedIn: 'root' })
export class PersSDKService {
private readonly sdk: PersSDK;
constructor() {
const httpClient = inject(HttpClient);
// Use createPersSDK factory or new PersSDK()
this.sdk = createPersSDK(
new AngularHttpClientAdapter(httpClient),
{
environment: 'production',
apiProjectKey: 'your-project-key',
authStorage: new IndexedDBTokenStorage() // Recommended
}
);
}
// Expose managers directly for clean component access
get auth() { return this.sdk.auth; }
get users() { return this.sdk.users; }
get tokens() { return this.sdk.tokens; }
get businesses() { return this.sdk.businesses; }
get campaigns() { return this.sdk.campaigns; }
get redemptions() { return this.sdk.redemptions; }
get transactions() { return this.sdk.transactions; }
get purchases() { return this.sdk.purchases; }
get web3() { return this.sdk.web3; }
// ... other managers as needed
}Node.js
No additional dependencies required. Use the dedicated Node.js entry point:
import { createNodeSDK } from '@explorins/pers-sdk/node';
// Convenience function for Node.js - auto-configures adapter
const sdk = createNodeSDK({
jwt: 'your-jwt-token',
projectKey: 'your-project-key',
environment: 'production'
});
// Ready to use
const campaigns = await sdk.campaigns.getActiveCampaigns();
const userClaims = await sdk.campaigns.getCampaignClaims({
userId: 'user-123'
});React Native
Use the dedicated React Native SDK (includes passkeys, secure storage, DPoP):
npm install @explorins/pers-sdk-react-native @react-native-async-storage/async-storage// In your root layout (e.g., _layout.tsx for Expo Router)
import { PersSDKProvider } from '@explorins/pers-sdk-react-native';
export default function RootLayout() {
return (
<PersSDKProvider config={{ apiProjectKey: 'your-project-key' }}>
<YourApp />
</PersSDKProvider>
);
}
// In your components - use hooks
import { useAuth, useTokens, useCampaigns } from '@explorins/pers-sdk-react-native';
function MyComponent() {
const { login, isAuthenticated } = useAuth();
const { getTokens } = useTokens();
const { getActiveCampaigns, claimCampaign } = useCampaigns();
// ...
}See @explorins/pers-sdk-react-native for full setup including passkey configuration.
Architecture
The SDK uses a clean Manager-Service pattern with three access levels:
// ═══════════════════════════════════════════════════════════════════════════
// MANAGER LAYER (Recommended - High-level, intuitive APIs)
// ═══════════════════════════════════════════════════════════════════════════
sdk.auth // Authentication & sessions
sdk.users // User profiles & management
sdk.userStatus // User status/tier management
sdk.tokens // Token types & configuration
sdk.businesses // Business operations
sdk.campaigns // Marketing campaigns
sdk.redemptions // Reward redemptions
sdk.transactions // Transaction history
sdk.purchases // Purchase/payment processing
sdk.analytics // Reporting & insights
sdk.tenants // Multi-tenant configuration
sdk.donations // Charitable giving
sdk.files // File upload/download operations
sdk.web3 // Blockchain operations
sdk.apiKeys // API key management (Admin)
// ═══════════════════════════════════════════════════════════════════════════
// SERVICE LAYER (Advanced - Full domain access)
// ═══════════════════════════════════════════════════════════════════════════
sdk.campaigns.getCampaignService() // Full CampaignService access
sdk.tokens.getTokenService() // Full TokenService access
sdk.businesses.getBusinessService() // Full BusinessService access
sdk.purchases.getPurchaseService() // Full PaymentService access
// ... each manager exposes its underlying service
// ═══════════════════════════════════════════════════════════════════════════
// API LAYER (Expert - Direct REST API access)
// ═══════════════════════════════════════════════════════════════════════════
const apiClient = sdk.api();
const customData = await apiClient.get<CustomType>('/custom-endpoint');
await apiClient.post('/custom-endpoint', { data: 'value' });Available Managers Reference
| Manager | Accessor | Primary Use Cases |
|---------|----------|-------------------|
| AuthManager | sdk.auth | Login, logout, token management, authentication status |
| UserManager | sdk.users | User profiles, account management |
| UserStatusManager | sdk.userStatus | User tiers, status levels |
| TokenManager | sdk.tokens | Token types, credit tokens, reward tokens |
| BusinessManager | sdk.businesses | Business discovery, details, types |
| CampaignManager | sdk.campaigns | Campaign discovery, claiming, user history |
| RedemptionManager | sdk.redemptions | Redeem rewards, redemption history |
| TransactionManager | sdk.transactions | Transaction history, details |
| PurchaseManager | sdk.purchases | Payment intents, purchase tokens |
| AnalyticsManager | sdk.analytics | Reporting, transaction analytics |
| TenantManager | sdk.tenants | Tenant config, client settings |
| DonationManager | sdk.donations | Donation types, charitable giving |
| FileManager | sdk.files | Signed URLs, media optimization |
| Web3Manager | sdk.web3 | Blockchain operations, token metadata |
| ApiKeyManager | sdk.apiKeys | API key CRUD (Admin only) |
Core Features
Event System
The SDK provides a platform-agnostic event system for subscribing to SDK-wide events. All events have a userMessage field ready for UI display.
// Subscribe to ALL events - one handler
const unsubscribe = sdk.events.subscribe((event) => {
showNotification(event.userMessage, event.level);
});
// Filter by domain and level
sdk.events.subscribe((event) => {
logToSentry(event);
}, { level: 'error' });
// Only transaction successes
sdk.events.subscribe((event) => {
playSuccessSound();
confetti();
}, { domain: 'transaction', level: 'success' });
// One-time event (auto-unsubscribe)
sdk.events.once((event) => {
console.log('First event:', event.type);
});
// Cleanup
unsubscribe();Event Domains
| Domain | Manager | Events |
|--------|---------|--------|
| authentication | sdk.auth | LOGIN_SUCCESS |
| user | sdk.users | PROFILE_UPDATED |
| business | sdk.businesses | BUSINESS_CREATED, BUSINESS_UPDATED, MEMBERSHIP_UPDATED |
| campaign | sdk.campaigns | CAMPAIGN_CLAIMED |
| redemption | sdk.redemptions | REDEMPTION_COMPLETED |
| transaction | sdk.transactions | TRANSACTION_CREATED, TRANSACTION_SUBMITTED |
Event Types
import type {
PersEvent, // Base event type (discriminated union)
SuccessEvent, // Success events (business domains)
ErrorEvent, // Error events (all domains including technical)
EventFilter, // Filter for subscriptions
EventHandler // Handler type (sync or async)
} from '@explorins/pers-sdk/core';
// Event structure
interface PersEvent {
id: string; // Unique event ID
timestamp: number; // Unix timestamp (ms)
domain: string; // Event domain
type: string; // Event type within domain
level: 'success' | 'error';
userMessage: string; // Ready for UI display
action?: string; // Suggested action
code?: string; // Backend error code
details?: object; // Additional data for logging
}Async Handler Support
Event handlers can be synchronous or asynchronous. Async errors are caught automatically:
sdk.events.subscribe(async (event) => {
await saveToDatabase(event); // Async operations supported
await sendAnalytics(event);
});Authentication
The SDK uses external JWT tokens for authentication. You provide a JWT from your authentication provider (Firebase, Auth0, Cognito, etc.), and the SDK exchanges it for PERS access tokens.
// Step 1: Get JWT from your auth provider (Firebase example)
import { getAuth } from 'firebase/auth';
const firebaseUser = getAuth().currentUser;
const externalJWT = await firebaseUser?.getIdToken();
// Step 2: Exchange JWT for PERS tokens
const authResult = await sdk.auth.loginWithToken(externalJWT, 'user');
console.log('User authenticated:', authResult.user.name);
// Admin login
const adminResult = await sdk.auth.loginWithToken(adminJWT, 'admin');
console.log('Admin authenticated:', adminResult.admin.email);
// Check authentication status (async - verifies with server)
const isAuth = await sdk.auth.isAuthenticated();
// Quick check if tokens exist locally (faster, less reliable)
const hasTokens = await sdk.auth.hasValidAuth();
// Get current user
const user = await sdk.auth.getCurrentUser();
// Logout / clear auth
await sdk.auth.clearAuth();How it works:
- Your app authenticates users via Firebase/Auth0/etc.
- You call
sdk.auth.loginWithToken(jwt)with that JWT - PERS validates the JWT and returns PERS-specific access/refresh tokens
- SDK stores tokens and handles automatic refresh
- All subsequent SDK calls are automatically authenticated
Security Features:
- DPoP (Demonstrating Proof-of-Possession): Enabled by default - binds tokens to client
- Automatic token refresh and validation
- Flexible storage strategies (LocalStorage, IndexedDB, Memory)
- Support for user and admin authentication flows
Business Management
// Get all active businesses
const businesses = await sdk.businesses.getActiveBusinesses();
// Get business by ID
const business = await sdk.businesses.getBusinessById('business-123');
// Get business types
const types = await sdk.businesses.getBusinessTypes();Campaign System
// Get active campaigns available for claiming
const campaigns = await sdk.campaigns.getActiveCampaigns();
// Get campaign details
const campaign = await sdk.campaigns.getCampaignById('campaign-123');
// Claim campaign rewards
const claim = await sdk.campaigns.claimCampaign({
campaignId: 'campaign-123',
businessId: 'business-456' // Optional: associated business
});
// Get user's campaign claim history
const userClaims = await sdk.campaigns.getUserClaims();Token Management
// Get all token types
const tokens = await sdk.tokens.getTokens();
// Get active credit token (main loyalty currency)
const creditToken = await sdk.tokens.getActiveCreditToken();
// Get reward tokens
const rewards = await sdk.tokens.getRewardTokens();
// Get status tokens (tier/achievement tokens)
const statusTokens = await sdk.tokens.getStatusTokens();
// Get token by blockchain contract
const token = await sdk.tokens.getTokenByContract('0x123...', 'token-id');User Token Balances (On-Chain)
Important Architecture Note: The PERS backend stores token definitions (contract addresses, ABIs, metadata), but does NOT store user token balances. User balances are queried directly from the blockchain via RPC calls using
sdk.web3.*methods.
Data Flow:
sdk.tokens.*→ Get token definitions (contract address, ABI, chainId) from PERS backendsdk.auth.getCurrentUser()→ Get user's wallet addresssdk.web3.*→ Query blockchain directly for user's token balance
Points Balance (ERC-20)
// Step 1: Get the credit token definition from PERS backend
const creditToken = await sdk.tokens.getActiveCreditToken();
// Step 2: Get user's wallet address
const user = await sdk.auth.getCurrentUser();
const walletAddress = user.wallets?.[0]?.address;
// Step 3: Query blockchain directly for balance
const balance = await sdk.web3.getTokenBalance({
accountAddress: walletAddress,
contractAddress: creditToken.contractAddress,
abi: creditToken.abi, // Raw ABI from backend works directly
chainId: creditToken.chainId,
tokenId: '' // Empty for ERC-20
});
console.log('Points balance:', balance.balance);
console.log('Has balance:', balance.hasBalance);Stamps Collection (ERC-721 / ERC-1155)
// Step 1: Get status token definitions (stamps/achievements)
const statusTokens = await sdk.tokens.getStatusTokens();
// Step 2: Get user's wallet address
const user = await sdk.auth.getCurrentUser();
const walletAddress = user.wallets?.[0]?.address;
// Step 3: Query blockchain for user's stamp collection
for (const stampToken of statusTokens) {
const collection = await sdk.web3.getTokenCollection({
accountAddress: walletAddress,
contractAddress: stampToken.contractAddress,
abi: stampToken.abi, // Raw ABI from backend works directly
chainId: stampToken.chainId,
maxTokens: 50 // Optional: limit results
});
// Filter to tokens with balance > 0
const ownedStamps = collection.tokens.filter(t => t.hasBalance && t.balance > 0);
console.log(`Stamps from ${stampToken.symbol}:`, ownedStamps.length);
// Each stamp includes metadata (name, description, image, etc.)
for (const stamp of ownedStamps) {
console.log(`- Token #${stamp.tokenId}: ${stamp.metadata?.name}`);
console.log(` Image: ${stamp.metadata?.imageUrl}`);
}
}Complete Example: Load All User Balances
async function loadUserTokenBalances(sdk: PersSDK) {
// Get user wallet
const user = await sdk.auth.getCurrentUser();
const walletAddress = user.wallets?.[0]?.address;
if (!walletAddress) throw new Error('User has no wallet');
// Get all token definitions
const [creditToken, statusTokens, rewardTokens] = await Promise.all([
sdk.tokens.getActiveCreditToken(),
sdk.tokens.getStatusTokens(),
sdk.tokens.getRewardTokens()
]);
// Query ERC-20 balance (Points)
const pointsBalance = creditToken ? await sdk.web3.getTokenBalance({
accountAddress: walletAddress,
contractAddress: creditToken.contractAddress,
abi: creditToken.abi,
chainId: creditToken.chainId,
tokenId: ''
}) : null;
// Query ERC-721/ERC-1155 collections (Stamps, Rewards)
const allNftTokens = [...(statusTokens || []), ...(rewardTokens || [])];
const collections = await Promise.all(
allNftTokens.map(token =>
sdk.web3.getTokenCollection({
accountAddress: walletAddress,
contractAddress: token.contractAddress,
abi: token.abi,
chainId: token.chainId,
maxTokens: 100
}).catch(() => null) // Handle individual failures gracefully
)
);
return {
points: pointsBalance,
stamps: collections.filter(Boolean)
};
}Transaction Request Building
The SDK provides factory functions for building transaction request DTOs. These are tree-shakeable and provide type-safe transaction creation:
import {
buildMintRequest,
buildBurnRequest,
buildTransferRequest,
buildPOSTransferRequest,
buildSubmissionRequest
} from '@explorins/pers-sdk/transaction';
import { AccountOwnerType } from '@explorins/pers-shared';
// Mint tokens to a recipient
const mintRequest = buildMintRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
recipientAccountId: 'user-123',
recipientAccountType: AccountOwnerType.USER
});
// Burn tokens
const burnRequest = buildBurnRequest({
amount: 50,
contractAddress: '0x...',
chainId: 137,
senderAccountId: 'user-123',
senderAccountType: AccountOwnerType.USER
});
// Transfer tokens between accounts
const transferRequest = buildTransferRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
senderAccountId: 'user-123',
senderAccountType: AccountOwnerType.USER,
recipientAccountId: 'business-456',
recipientAccountType: AccountOwnerType.BUSINESS
});
// Submit the transaction
const result = await sdk.transactions.createTransaction(mintRequest);POS Transaction Flow
For Point-of-Sale scenarios where a business submits a transaction on behalf of a user, use buildPOSTransferRequest:
import { buildPOSTransferRequest } from '@explorins/pers-sdk/transaction';
// POS flow: User pays business, business authorized to submit
const posRequest = buildPOSTransferRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
userId: 'user-123', // User sending tokens
businessId: 'business-456' // Business receiving & authorized to submit
});
// This automatically sets:
// - engagedBusinessId: 'business-456' (for reporting)
// - authorizedSubmitterId: 'business-456' (can submit the signed tx)
// - authorizedSubmitterType: AccountOwnerType.BUSINESS
const result = await sdk.transactions.createTransaction(posRequest);For custom authorization scenarios, use buildTransferRequest with manual POS fields:
import { buildTransferRequest, type POSAuthorizationOptions } from '@explorins/pers-sdk/transaction';
const customPOSRequest = buildTransferRequest({
amount: 100,
contractAddress: '0x...',
chainId: 137,
senderAccountId: 'user-123',
senderAccountType: AccountOwnerType.USER,
recipientAccountId: 'business-456',
recipientAccountType: AccountOwnerType.BUSINESS,
// POS authorization fields
engagedBusinessId: 'business-456',
authorizedSubmitterId: 'business-456',
authorizedSubmitterType: AccountOwnerType.BUSINESS
});| POS Field | Type | Description |
|-----------|------|-------------|
| engagedBusinessId | string | Business commercially involved (for stats/reporting) |
| authorizedSubmitterId | string | Entity authorized to submit the signed transaction |
| authorizedSubmitterType | AccountOwnerType | Type of authorized submitter (USER or BUSINESS) |
### Purchase/Payment Processing
```typescript
// Create payment intent
const intent = await sdk.purchases.createPaymentIntent(
100, // amount
'usd', // currency
'[email protected]', // email
'Token Purchase' // description
);
// Get user's purchase history
const purchases = await sdk.purchases.getAllUserPurchases();
// Get available purchase tokens
const purchaseTokens = await sdk.purchases.getActivePurchaseTokens();Configuration
API Project Key (Required)
The apiProjectKey is a tenant-specific identifier that associates your application with a PERS tenant (organization). You must obtain this from the PERS admin dashboard or from your PERS account manager.
// Example project key format (64-character hex string)
apiProjectKey: 'e3e16b5863f0a042b949650d236a37b0758bd51177463d627921112d2291fe01'Full Configuration Example
import { PersSDK } from '@explorins/pers-sdk';
import { IndexedDBTokenStorage } from '@explorins/pers-sdk/core';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
const sdk = new PersSDK(new BrowserFetchClientAdapter(), {
// Environment: 'development' | 'staging' | 'production'
environment: 'production', // Default: 'production'
// Project key for API authentication (required)
apiProjectKey: 'your-project-key',
// API version (currently only v2 supported)
apiVersion: 'v2', // Default: 'v2'
// Request timeout in milliseconds
timeout: 30000, // Default: 30000
// Retry attempts for failed requests
retries: 3, // Default: 3
// Token storage strategy (recommended: IndexedDB)
authStorage: new IndexedDBTokenStorage(), // Default: LocalStorage
// DPoP (Demonstrating Proof-of-Possession) configuration
dpop: {
enabled: true, // Default: true (recommended for security)
},
// Authentication type for auto-created provider
authType: 'user', // 'user' | 'admin', Default: 'user'
// Token refresh margin (seconds before expiry to refresh)
tokenRefreshMargin: 60, // Default: 60
// Background refresh threshold (seconds)
backgroundRefreshThreshold: 30 // Default: 30
});Configuration Options Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| environment | 'development' \| 'staging' \| 'production' | 'production' | API environment target |
| apiProjectKey | string | - | Required. Project key for API authentication |
| apiVersion | 'v2' | 'v2' | API version |
| timeout | number | 30000 | Request timeout (ms) |
| retries | number | 3 | Retry attempts |
| authStorage | TokenStorage | LocalStorage | Token storage implementation |
| dpop.enabled | boolean | true | Enable DPoP security |
| authType | 'user' \| 'admin' | 'user' | Authentication type |
| authProvider | PersAuthProvider | auto-created | Custom auth provider (overrides authType) |
Advanced Authentication
Using IndexedDB Storage (Recommended)
For better security and performance, use IndexedDB instead of LocalStorage:
import { createPersSDK } from '@explorins/pers-sdk';
import { IndexedDBTokenStorage } from '@explorins/pers-sdk/core';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
environment: 'production',
apiProjectKey: 'your-key',
authStorage: new IndexedDBTokenStorage() // Secure, async storage
});Custom Storage Implementation
Implement the TokenStorage interface for custom storage backends:
import type { TokenStorage } from '@explorins/pers-sdk/core';
import { AUTH_STORAGE_KEYS, DPOP_STORAGE_KEYS } from '@explorins/pers-sdk/core';
class CustomStorage implements TokenStorage {
// CRITICAL: Set to 'false' for string-only backends (LocalStorage-like)
// Set to 'true' for object-capable backends (IndexedDB-like)
readonly supportsObjects = false;
async get(key: string): Promise<unknown | null> {
// Use AUTH_STORAGE_KEYS.ACCESS_TOKEN, DPOP_STORAGE_KEYS.PRIVATE, etc.
return yourBackend.get(key);
}
async set(key: string, value: unknown): Promise<void> {
await yourBackend.set(key, value);
}
async remove(key: string): Promise<void> {
await yourBackend.remove(key);
}
async clear(): Promise<void> {
await yourBackend.clear();
}
}Error Handling
The SDK provides structured error types for consistent error handling:
import {
PersApiError,
AuthenticationError,
ErrorUtils
} from '@explorins/pers-sdk/core';
try {
const user = await sdk.auth.getCurrentUser();
} catch (error) {
// Check for specific error types
if (error instanceof AuthenticationError) {
// Handle authentication failure (401)
console.error('Auth failed:', error.message);
console.error('Endpoint:', error.endpoint);
console.error('User message:', error.userMessage);
// Redirect to login...
} else if (error instanceof PersApiError) {
// Handle general API errors
console.error('API Error:', error.message);
console.error('Status:', error.status);
console.error('Retryable:', error.retryable);
} else {
// Handle unexpected errors
console.error('Unexpected error:', error);
}
}
// Utility functions for error inspection
const status = ErrorUtils.getStatus(error); // Extract status code
const message = ErrorUtils.getMessage(error); // Extract error message
const retryable = ErrorUtils.isRetryable(error); // Check if retryableError Types Reference
| Error Class | Status | Use Case |
|-------------|--------|----------|
| PersApiError | Various | General API request failures |
| AuthenticationError | 401 | Authentication/authorization failures |
| NetworkError | - | Network connectivity issues |
| TokenRefreshNeeded | - | Internal: token refresh required |
| LogoutRequired | - | Internal: session invalidated |
Bundle Size
- Installed to node_modules: ~1.9 MB unpacked (includes both ESM + CJS builds, TypeScript definitions, source maps)
- What your bundler actually includes:
- ESM build: ~450 KB (Vite, Rollup, modern Webpack)
- CJS build: ~470 KB (legacy Node.js/Webpack)
- Your bundler only includes ONE of these, not both
- TypeScript definitions: ~510 KB (used by IDE/compiler, not bundled into your app)
- Source maps: ~440 KB (used for debugging, typically excluded from production)
- With Web3 Features: +1.5 MB additional when installing optional
@explorins/web3-ts+etherspeer dependencies - Tree-shaking: The main
PersSDKclass includes all managers. For smaller bundles, import from domain-specific entry points (e.g.,@explorins/pers-sdk/campaign) - Zero bundled dependencies: All dependencies are peer dependencies
Bottom line: Your production bundle gets ~450 KB from this SDK (ESM), not 1.9 MB. The larger size is what's stored in node_modules, which includes both module formats, type definitions, and development tools.
TypeScript Types
All domain interfaces are exported from @explorins/pers-shared. Import types directly for strong typing:
import type {
// User & Auth
UserDTO,
SessionAuthContextResponseDTO,
// Business
BusinessDTO,
BusinessTypeDTO,
// Campaigns
CampaignDTO,
CampaignClaimDTO,
CampaignClaimRequestDTO,
// Tokens
TokenDTO,
TokenMetadataDTO,
// Transactions
TransactionDTO,
// Redemptions
RedemptionDTO,
RedemptionRedeemDTO,
// Payments
PaymentIntentDTO,
PurchaseDTO,
PurchaseTokenDTO,
// Tenant
TenantDTO,
TenantClientConfigDTO
} from '@explorins/pers-shared';Key Interfaces Reference
| Domain | Interface | Description |
|--------|-----------|-------------|
| Auth | UserDTO | Authenticated user profile |
| Auth | SessionAuthContextResponseDTO | Login response with tokens + user/admin |
| Business | BusinessDTO | Business entity with details |
| Business | BusinessTypeDTO | Business category/type definition |
| Campaign | CampaignDTO | Campaign with rewards and rules |
| Campaign | CampaignClaimDTO | User's claim record |
| Campaign | CampaignClaimRequestDTO | Request body for claiming |
| Token | TokenDTO | Token type definition (credit, reward, status) |
| Token | TokenMetadataDTO | On-chain token metadata |
| Transaction | TransactionDTO | Transaction record |
| Redemption | RedemptionDTO | Redemption offer |
| Redemption | RedemptionRedeemDTO | Redemption result |
| Payment | PaymentIntentDTO | Stripe payment intent |
| Payment | PurchaseDTO | Purchase record |
| Tenant | TenantDTO | Tenant configuration |
Usage Example
import { createPersSDK } from '@explorins/pers-sdk';
import { BrowserFetchClientAdapter } from '@explorins/pers-sdk/platform-adapters';
import type { CampaignDTO, CampaignClaimRequestDTO } from '@explorins/pers-shared';
const sdk = createPersSDK(new BrowserFetchClientAdapter(), {
apiProjectKey: 'your-key'
});
// Type-safe campaign operations
const response = await sdk.campaigns.getActiveCampaigns();
const campaigns: CampaignDTO[] = response.data;
const claimRequest: CampaignClaimRequestDTO = {
campaignId: campaigns[0].id,
businessId: 'business-123'
};
const claim = await sdk.campaigns.claimCampaign(claimRequest);Migration Guide
v2.0.0 Breaking Changes - Pagination
Version 2.0.0 introduces standardized pagination across all list endpoints. Previously, endpoints returned raw arrays. Now they return PaginatedResponseDTO<T> with pagination metadata.
What Changed
All methods returning lists now return paginated responses:
// ❌ OLD (v1.x) - Direct array
const businesses: BusinessDTO[] = await sdk.businesses.getActiveBusinesses();
// ✅ NEW (v2.x) - Paginated response
const response: PaginatedResponseDTO<BusinessDTO> = await sdk.businesses.getActiveBusinesses();
const businesses: BusinessDTO[] = response.data;Affected Methods
| Manager | Method | Return Type |
|---------|--------|-------------|
| businesses | getActiveBusinesses() | PaginatedResponseDTO<BusinessDTO> |
| businesses | getBusinessTypes() | PaginatedResponseDTO<BusinessTypeDTO> |
| campaigns | getActiveCampaigns() | PaginatedResponseDTO<CampaignDTO> |
| campaigns | getUserClaims() | PaginatedResponseDTO<CampaignClaimDTO> |
| tokens | getTokens() | PaginatedResponseDTO<TokenDTO> |
| tokens | getRewardTokens() | PaginatedResponseDTO<TokenDTO> |
| tokens | getStatusTokens() | PaginatedResponseDTO<TokenDTO> |
| redemptions | getRedemptions() | PaginatedResponseDTO<RedemptionDTO> |
| redemptions | getUserRedemptions() | PaginatedResponseDTO<RedemptionDTO> |
| transactions | getTransactionHistory() | PaginatedResponseDTO<TransactionDTO> |
| purchases | getAllUserPurchases() | PaginatedResponseDTO<PurchaseDTO> |
| purchases | getActivePurchaseTokens() | PaginatedResponseDTO<PurchaseTokenDTO> |
| donations | getDonationTypes() | PaginatedResponseDTO<DonationTypeDTO> |
PaginatedResponseDTO Structure
import type { PaginatedResponseDTO } from '@explorins/pers-shared';
interface PaginatedResponseDTO<T> {
data: T[]; // Array of results
pagination: {
currentPage: number; // Current page number (1-indexed)
pageSize: number; // Items per page
totalItems: number; // Total number of items across all pages
totalPages: number; // Total number of pages
};
}Migration Examples
Before (v1.x):
const businesses = await sdk.businesses.getActiveBusinesses();
console.log('Business count:', businesses.length);
businesses.forEach(b => console.log(b.name));After (v2.x):
const response = await sdk.businesses.getActiveBusinesses();
console.log('Business count:', response.data.length);
console.log('Total businesses:', response.pagination.totalItems);
response.data.forEach(b => console.log(b.name));With Pagination Parameters:
// Fetch page 2 with 20 items per page
const response = await sdk.businesses.getActiveBusinesses({
page: 2,
pageSize: 20
});
console.log(`Page ${response.pagination.currentPage} of ${response.pagination.totalPages}`);
console.log(`Showing ${response.data.length} businesses`);Quick Fix for Existing Code
If you want minimal code changes, extract .data immediately:
// Quick adaptation
const businesses = (await sdk.businesses.getActiveBusinesses()).data;
const campaigns = (await sdk.campaigns.getActiveCampaigns()).data;
const tokens = (await sdk.tokens.getTokens()).data;Benefits of Pagination
- Performance: Load only what you need, not entire datasets
- Consistency: All list endpoints follow the same pattern
- Metadata: Access total counts without loading all items
- UX: Build proper pagination UI components
Documentation
- Getting Started Guide: https://docs.pers.ninja/1.intro
- API Reference: https://docs.pers.ninja/sdk
- TypeDoc API Docs: Generated with
npm run docs
Related Packages
| Package | Description | |---------|-------------| | @explorins/pers-sdk-react-native | React Native integration with passkey support | | @explorins/pers-shared | Shared types, interfaces, and DTOs |
License
MIT License
