Learn how to build applications that can serve multiple DRIP communities simultaneously. This guide covers the technical and business aspects of multi-realm app development.

Overview

Multi-realm apps are applications that can be authorized and used by multiple DRIP communities. Unlike realm clients that are tied to a single community, multi-realm apps can scale across the entire DRIP ecosystem.
This guide assumes you’ve already read the App Development guide and understand the basics of DRIP app creation.

Key Differences from Single-Realm Apps

Single-Realm Apps

  • Fixed realm context
  • Direct API key access
  • Immediate deployment
  • Simple authorization

Multi-Realm Apps

  • Dynamic realm context
  • App client credentials
  • Publishing required for cross-realm authorization
  • Complex authorization flow

Authorization Architecture

Two-Tier Authorization

Multi-realm apps use a two-tier authorization system:

Managing Authorized Realms

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();
  }
}

Data Isolation and Management

Per-Realm Data Storage

Each realm’s data should be properly isolated:
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);
  }
}

Cross-Realm Analytics

Aggregate data across multiple realms while respecting boundaries:
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
      }));
  }
}

User Experience Patterns

Realm Selection Interface

Create a smooth realm selection experience:
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}`);
  }
}

Context-Aware UI

Adapt your UI based on available permissions:
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>
    `;
  }
}

Scaling Considerations

Database Design

Structure your database to handle multiple realms efficiently:
-- 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()
);

Caching Strategy

Implement realm-aware caching:
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();
  }
}

Business Model Considerations

Pricing Strategies

Different approaches for multi-realm apps:

Revenue Tracking

Track revenue per realm for better insights:
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
    };
  }
}

Testing Multi-Realm Apps

Testing Strategy

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');
  }
}

Deployment and Monitoring

Health Checks for Multi-Realm Apps

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
    };
  }
}

Next Steps

Building something cool? Share your multi-realm app in our Discord community and get feedback from other developers! 🚀