Build applications that work across multiple DRIP communities 🌐
class MultiRealmAppClient {
constructor(appClientSecret) {
this.appClientSecret = appClientSecret;
this.authorizedRealms = new Map();
this.baseUrl = 'https://api.drip.re/api/v1';
}
async loadAuthorizedRealms() {
const response = await fetch(`${this.baseUrl}/apps/:appId/authorized-realms`, {
headers: {
'Authorization': `Bearer ${this.appClientSecret}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
// Store authorized realms with their permissions
for (const authorization of data.data) {
this.authorizedRealms.set(authorization.realmId, {
realm: authorization.realm,
approvedScopes: authorization.approvedScopes,
authorizedAt: authorization.authorizedAt,
settings: authorization.settings || {}
});
}
return this.authorizedRealms;
}
isAuthorizedForRealm(realmId) {
return this.authorizedRealms.has(realmId);
}
hasScope(realmId, scope) {
const auth = this.authorizedRealms.get(realmId);
return auth && auth.approvedScopes.includes(scope);
}
async makeRealmRequest(realmId, method, endpoint, data = null) {
if (!this.isAuthorizedForRealm(realmId)) {
throw new Error(`Not authorized for realm ${realmId}`);
}
const url = `${this.baseUrl}${endpoint}`;
const options = {
method,
headers: {
'Authorization': `Bearer ${this.appClientSecret}`,
'Content-Type': 'application/json'
}
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
throw new Error(`API Error: ${response.status} - ${error.message}`);
}
return response.json();
}
}
class RealmDataManager {
constructor(appClient) {
this.appClient = appClient;
this.realmData = new Map();
}
async initializeRealmData(realmId) {
if (this.realmData.has(realmId)) {
return this.realmData.get(realmId);
}
// Load realm-specific configuration
const realmInfo = await this.appClient.makeRealmRequest(
realmId,
'GET',
`/realms/${realmId}`
);
// Initialize realm data structure
const realmData = {
info: realmInfo,
settings: await this.loadRealmSettings(realmId),
cache: new Map(),
lastUpdated: Date.now()
};
this.realmData.set(realmId, realmData);
return realmData;
}
async loadRealmSettings(realmId) {
// Load app-specific settings for this realm
// This could come from your own database or DRIP's app settings
return {
pointsPerAction: 10,
enableNotifications: true,
customBranding: {
primaryColor: '#667eea',
logo: null
}
};
}
async updateRealmSettings(realmId, settings) {
const realmData = await this.initializeRealmData(realmId);
realmData.settings = { ...realmData.settings, ...settings };
// Persist to your database
await this.persistRealmSettings(realmId, realmData.settings);
}
getRealmData(realmId) {
return this.realmData.get(realmId);
}
}
class CrossRealmAnalytics {
constructor(appClient) {
this.appClient = appClient;
}
async getAggregatedStats() {
const authorizedRealms = this.appClient.authorizedRealms;
const stats = {
totalRealms: authorizedRealms.size,
totalMembers: 0,
totalPoints: 0,
realmBreakdown: []
};
// Process each realm in parallel
const realmPromises = Array.from(authorizedRealms.keys()).map(async (realmId) => {
try {
if (!this.appClient.hasScope(realmId, 'members:read')) {
return null; // Skip realms without permission
}
const members = await this.appClient.makeRealmRequest(
realmId,
'GET',
`/realm/${realmId}/members/search?type=drip-id&values=all`
);
const realmStats = {
realmId,
realmName: authorizedRealms.get(realmId).realm.name,
memberCount: members.data.length,
totalPoints: members.data.reduce((sum, member) => {
return sum + (member.pointBalances[0]?.balance || 0);
}, 0)
};
stats.totalMembers += realmStats.memberCount;
stats.totalPoints += realmStats.totalPoints;
return realmStats;
} catch (error) {
console.error(`Error fetching stats for realm ${realmId}:`, error);
return null;
}
});
const realmResults = await Promise.all(realmPromises);
stats.realmBreakdown = realmResults.filter(result => result !== null);
return stats;
}
async getTopMembersAcrossRealms(limit = 10) {
const authorizedRealms = this.appClient.authorizedRealms;
const allMembers = [];
for (const [realmId, auth] of authorizedRealms) {
if (!this.appClient.hasScope(realmId, 'members:read')) {
continue;
}
try {
const members = await this.appClient.makeRealmRequest(
realmId,
'GET',
`/realm/${realmId}/members/search?type=drip-id&values=all`
);
// Add realm context to each member
for (const member of members.data) {
allMembers.push({
...member,
realmId,
realmName: auth.realm.name
});
}
} catch (error) {
console.error(`Error fetching members for realm ${realmId}:`, error);
}
}
// Sort by points and return top members
return allMembers
.filter(member => member.pointBalances && member.pointBalances.length > 0)
.sort((a, b) => {
const aPoints = a.pointBalances[0]?.balance || 0;
const bPoints = b.pointBalances[0]?.balance || 0;
return bPoints - aPoints;
})
.slice(0, limit)
.map((member, index) => ({
rank: index + 1,
name: member.displayName || member.username,
points: member.pointBalances[0].balance,
realmName: member.realmName,
realmId: member.realmId
}));
}
}
class RealmSelector {
constructor(appClient) {
this.appClient = appClient;
this.currentRealmId = null;
}
async renderRealmSelector() {
const realms = Array.from(this.appClient.authorizedRealms.values());
return `
<div class="realm-selector">
<h3>Select Community</h3>
<div class="realm-grid">
${realms.map(auth => `
<div class="realm-card" onclick="selectRealm('${auth.realm.id}')">
<img src="${auth.realm.imageUrl || '/default-realm.png'}" alt="${auth.realm.name}">
<h4>${auth.realm.name}</h4>
<p>${auth.realm.memberCount || 0} members</p>
<div class="scopes">
${auth.approvedScopes.slice(0, 3).map(scope =>
`<span class="scope-badge">${scope}</span>`
).join('')}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
selectRealm(realmId) {
if (!this.appClient.isAuthorizedForRealm(realmId)) {
throw new Error('Not authorized for this realm');
}
this.currentRealmId = realmId;
this.onRealmChanged(realmId);
}
onRealmChanged(realmId) {
// Override this method to handle realm changes
console.log(`Switched to realm: ${realmId}`);
}
}
class ContextAwareUI {
constructor(appClient, realmId) {
this.appClient = appClient;
this.realmId = realmId;
}
renderMemberActions() {
const canRead = this.appClient.hasScope(this.realmId, 'members:read');
const canWrite = this.appClient.hasScope(this.realmId, 'members:write');
const canAwardPoints = this.appClient.hasScope(this.realmId, 'points:write');
if (!canRead) {
return '<p>No member permissions for this realm</p>';
}
return `
<div class="member-actions">
${canRead ? '<button onclick="loadMembers()">View Members</button>' : ''}
${canWrite ? '<button onclick="editMember()">Edit Member</button>' : ''}
${canAwardPoints ? '<button onclick="awardPoints()">Award Points</button>' : ''}
</div>
`;
}
renderBasedOnPermissions() {
const auth = this.appClient.authorizedRealms.get(this.realmId);
const scopes = auth.approvedScopes;
const features = {
analytics: scopes.includes('members:read'),
pointManagement: scopes.includes('points:write'),
questManagement: scopes.includes('quests:write'),
storeManagement: scopes.includes('store:write')
};
return `
<div class="app-features">
${features.analytics ? this.renderAnalytics() : ''}
${features.pointManagement ? this.renderPointManagement() : ''}
${features.questManagement ? this.renderQuestManagement() : ''}
${features.storeManagement ? this.renderStoreManagement() : ''}
</div>
`;
}
}
-- Realm-specific tables
CREATE TABLE realm_settings (
realm_id VARCHAR(24) PRIMARY KEY,
app_settings JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE realm_analytics (
id SERIAL PRIMARY KEY,
realm_id VARCHAR(24) NOT NULL,
metric_name VARCHAR(100) NOT NULL,
metric_value NUMERIC NOT NULL,
recorded_at TIMESTAMP DEFAULT NOW(),
INDEX idx_realm_metric (realm_id, metric_name),
INDEX idx_recorded_at (recorded_at)
);
CREATE TABLE cross_realm_data (
id SERIAL PRIMARY KEY,
data_type VARCHAR(50) NOT NULL,
aggregated_value JSONB NOT NULL,
realm_count INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
class MultiRealmCache {
constructor(ttl = 300000) { // 5 minutes
this.cache = new Map();
this.ttl = ttl;
}
getKey(realmId, type, identifier) {
return `${realmId}:${type}:${identifier}`;
}
set(realmId, type, identifier, data) {
const key = this.getKey(realmId, type, identifier);
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
get(realmId, type, identifier) {
const key = this.getKey(realmId, type, identifier);
const cached = this.cache.get(key);
if (!cached) {
return null;
}
if (Date.now() - cached.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
invalidateRealm(realmId) {
// Remove all cached data for a specific realm
for (const key of this.cache.keys()) {
if (key.startsWith(`${realmId}:`)) {
this.cache.delete(key);
}
}
}
clear() {
this.cache.clear();
}
}
Per-Realm Pricing
class PerRealmPricing {
calculatePrice(realmCount) {
const basePrice = 9.99; // Base price for first realm
const additionalRealmPrice = 4.99; // Price per additional realm
if (realmCount <= 1) {
return basePrice;
}
return basePrice + (realmCount - 1) * additionalRealmPrice;
}
}
Usage-Based Pricing
class UsageBasedPricing {
calculatePrice(totalApiCalls, totalMembers) {
const apiCallPrice = 0.001; // $0.001 per API call
const memberPrice = 0.05; // $0.05 per member per month
return (totalApiCalls * apiCallPrice) + (totalMembers * memberPrice);
}
}
Feature-Based Pricing
class FeatureBasedPricing {
calculatePrice(realms, features) {
let totalPrice = 0;
for (const realm of realms) {
let realmPrice = 5.99; // Base price per realm
if (features.analytics) realmPrice += 2.99;
if (features.automation) realmPrice += 4.99;
if (features.customBranding) realmPrice += 1.99;
totalPrice += realmPrice;
}
return totalPrice;
}
}
class RevenueTracker {
constructor() {
this.realmRevenue = new Map();
}
recordRealmRevenue(realmId, amount, period) {
if (!this.realmRevenue.has(realmId)) {
this.realmRevenue.set(realmId, {
totalRevenue: 0,
monthlyRevenue: [],
firstPayment: Date.now()
});
}
const realmData = this.realmRevenue.get(realmId);
realmData.totalRevenue += amount;
realmData.monthlyRevenue.push({
period,
amount,
timestamp: Date.now()
});
}
getRealmMetrics(realmId) {
const data = this.realmRevenue.get(realmId);
if (!data) return null;
const monthlyAverage = data.monthlyRevenue.reduce((sum, payment) =>
sum + payment.amount, 0) / data.monthlyRevenue.length;
return {
totalRevenue: data.totalRevenue,
monthlyAverage,
paymentCount: data.monthlyRevenue.length,
customerLifetime: Date.now() - data.firstPayment
};
}
}
class MultiRealmTester {
constructor() {
this.testRealms = [
{ id: 'test-realm-1', name: 'Test Realm 1', scopes: ['members:read', 'points:write'] },
{ id: 'test-realm-2', name: 'Test Realm 2', scopes: ['members:read'] },
{ id: 'test-realm-3', name: 'Test Realm 3', scopes: ['members:read', 'members:write', 'points:write'] }
];
}
async testCrossRealmFunctionality() {
const appClient = new MultiRealmAppClient(process.env.TEST_APP_CLIENT_SECRET);
// Mock authorized realms for testing
for (const testRealm of this.testRealms) {
appClient.authorizedRealms.set(testRealm.id, {
realm: testRealm,
approvedScopes: testRealm.scopes,
authorizedAt: new Date().toISOString()
});
}
// Test realm-specific operations
for (const testRealm of this.testRealms) {
console.log(`Testing realm: ${testRealm.name}`);
// Test permission checking
assert(appClient.hasScope(testRealm.id, 'members:read'));
if (appClient.hasScope(testRealm.id, 'points:write')) {
// Test point operations
console.log('✅ Can award points in this realm');
} else {
console.log('❌ Cannot award points in this realm');
}
}
// Test cross-realm analytics
const analytics = new CrossRealmAnalytics(appClient);
const stats = await analytics.getAggregatedStats();
assert(stats.totalRealms === this.testRealms.length);
console.log('✅ Cross-realm analytics working');
}
}
async function multiRealmHealthCheck() {
const appClient = new MultiRealmAppClient(process.env.APP_CLIENT_SECRET);
try {
await appClient.loadAuthorizedRealms();
const healthStatus = {
status: 'healthy',
timestamp: new Date().toISOString(),
authorizedRealms: appClient.authorizedRealms.size,
realmStatus: {}
};
// Test a sample of realms
const realmIds = Array.from(appClient.authorizedRealms.keys()).slice(0, 3);
for (const realmId of realmIds) {
try {
await appClient.makeRealmRequest(realmId, 'GET', `/realms/${realmId}`);
healthStatus.realmStatus[realmId] = 'healthy';
} catch (error) {
healthStatus.realmStatus[realmId] = 'unhealthy';
healthStatus.status = 'degraded';
}
}
return healthStatus;
} catch (error) {
return {
status: 'unhealthy',
timestamp: new Date().toISOString(),
error: error.message
};
}
}
Was this page helpful?