Skip to content

Deployment and Integration Guide

Internal reference for deploying the CG Lounge License Server and wiring it into cglounge.studio.


Part 1: Secrets and Configuration

Required Secrets

Secret Purpose Source
admin.secret Protects all admin API endpoints You generate this
stripe.webhook_secret Verifies Stripe webhook signatures Stripe Dashboard
cglounge.webhook_secret Verifies cglounge backend webhook calls You generate this, share with cglounge backend

Generating Secrets

openssl rand -hex 32
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Both produce a 64-character hex string suitable for any of the three secrets.

Getting the Stripe Webhook Secret

  1. Go to Stripe Dashboard > Developers > Webhooks
  2. Click Add endpoint
  3. Set the endpoint URL:
    https://us-central1-YOUR-PROJECT.cloudfunctions.net/stripeWebhook
    
  4. Select the following events:
  5. checkout.session.completed
  6. charge.refunded
  7. charge.dispute.created
  8. invoice.paid
  9. customer.subscription.deleted
  10. Save the endpoint, then click Reveal under Signing secret
  11. Copy the whsec_... value. That is your stripe.webhook_secret.

Setting Secrets in Firebase

# Admin secret (you create this)
firebase functions:config:set admin.secret="your-32-char-random-string"

# Stripe webhook secret (from Stripe Dashboard)
firebase functions:config:set stripe.webhook_secret="whsec_your_stripe_secret"

# cglounge webhook secret (you create this, share with cglounge backend)
firebase functions:config:set cglounge.webhook_secret="another-random-string"

# Verify the config
firebase functions:config:get

Redeploy after config changes

Firebase functions read config at cold start. After any functions:config:set, run firebase deploy --only functions to apply the new values.


Part 2: Deployment

Prerequisites

Requirement Check
Firebase CLI installed npm install -g firebase-tools
Logged in firebase login
Project selected firebase use YOUR_PROJECT_ID
Secrets configured See Part 1

Deploy Command

firebase deploy --only functions,firestore:indexes

You can split this into two steps if needed:

firebase deploy --only firestore:indexes
firebase deploy --only functions

Endpoint URLs After Deploy

All endpoints live under:

https://us-central1-YOUR-PROJECT.cloudfunctions.net/
Endpoint Type
/activate Client
/validate Client
/deactivate Client
/heartbeat Client
/verify Client
/stripeWebhook Webhook
/cgloungeWebhook Webhook
/createLicense Admin
/getLicense Admin
/listLicenses Admin
/revokeLicense Admin
/reinstateLicense Admin
/resolveLicense Admin
/createProduct Admin
/createVariant Admin

Part 3: cglounge Integration

Architecture

┌──────────────────────────────────────────────────────────────────────┐
│                          cglounge.studio                              │
│                                                                        │
│  Frontend                          Backend                            │
│  ├── /account/licenses             ├── onPurchaseComplete()           │
│  │   (user license management)     │   onRefund()                     │
│  └── /dashboard/licenses           │   onDispute()                    │
│      (creator admin panel)         └── licenseServer.ts (API client)  │
│                                                                        │
└───────────────────────────────────────┬──────────────────────────────┘
                                        │ HTTPS
┌──────────────────────────────────────────────────────────────────────┐
│                      CG Lounge License Server (Firebase)                     │
│                                                                        │
│  Webhooks                    Admin API              Client API        │
│  /cgloungeWebhook            /createLicense         /activate        │
│  /stripeWebhook              /revokeLicense         /validate        │
│                              /listLicenses          /deactivate      │
│                              /resolveLicense        /heartbeat       │
│                                                                        │
└──────────────────────────────────────────────────────────────────────┘

Step 1: Store the Admin Secret

Add these to the cglounge backend environment:

CG_LICENSE_SERVER_URL=https://us-central1-YOUR-PROJECT.cloudfunctions.net
CG_LICENSE_ADMIN_SECRET=your-admin-secret
CG_LICENSE_WEBHOOK_SECRET=your-cglounge-webhook-secret

Step 2: Create Product When Creator Enables Licensing

Call this when a creator saves licensing settings on their product page:

async function setupProductLicensing(productSlug, creatorId, variants) {
  const BASE = process.env.CG_LICENSE_SERVER_URL;
  const SECRET = process.env.CG_LICENSE_ADMIN_SECRET;

  // Create the product
  await fetch(`${BASE}/createProduct`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: productSlug,
      slug: productSlug,
      creatorId,
      adminSecret: SECRET,
    }),
  });

  // Create each variant (indie, studio, site, etc.)
  for (const v of variants) {
    await fetch(`${BASE}/createVariant`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        productId: productSlug,
        name: v.name,
        licenseType: v.licenseType,   // "per-machine", "floating", or "site"
        maxMachines: v.maxMachines,
        maxConcurrent: v.maxConcurrent,
        durationDays: v.durationDays,  // omit for perpetual
        price: v.price,
        adminSecret: SECRET,
      }),
    });
  }
}

Step 3: Create License on Purchase

Fire this after a successful payment confirmation:

async function onPurchaseComplete(purchase) {
  const BASE = process.env.CG_LICENSE_SERVER_URL;

  const response = await fetch(`${BASE}/cgloungeWebhook`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Secret': process.env.CG_LICENSE_WEBHOOK_SECRET,
    },
    body: JSON.stringify({
      type: 'purchase.completed',
      email: purchase.buyerEmail,
      productId: purchase.productSlug,
      variant: purchase.variantName,    // "indie", "studio", etc.
      purchaseId: purchase.orderId,
      discountCode: purchase.discountCode,
      durationDays: purchase.durationDays,  // for subscription tiers
      amount: purchase.amount,
      currency: purchase.currency,
    }),
  });

  const result = await response.json();

  if (result.licenseKey) {
    await saveLicenseKeyToPurchaseRecord(purchase.orderId, result.licenseKey);
  }
}

Dedup protection

The webhook endpoint uses purchaseId for dedup. Sending the same purchaseId twice returns a 409 without creating a duplicate license. Safe to retry on network failures.

Step 4: Handle Refunds and Disputes

async function onRefund(orderId) {
  await fetch(`${BASE}/cgloungeWebhook`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Secret': process.env.CG_LICENSE_WEBHOOK_SECRET,
    },
    body: JSON.stringify({
      type: 'purchase.refunded',
      purchaseId: orderId,
    }),
  });
}

async function onDispute(orderId) {
  await fetch(`${BASE}/cgloungeWebhook`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Secret': process.env.CG_LICENSE_WEBHOOK_SECRET,
    },
    body: JSON.stringify({
      type: 'purchase.disputed',
      purchaseId: orderId,
    }),
  });
}

Refund sets the license to revoked. Dispute sets it to suspended pending manual review.

Step 5: User License Management UI

For the /account/licenses page:

// List all licenses for a user by email
async function getUserLicenses(userEmail) {
  const res = await fetch(`${BASE}/listLicenses`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: userEmail,
      adminSecret: process.env.CG_LICENSE_ADMIN_SECRET,
    }),
  });
  return res.json();
}

// Fetch full license detail including active machines
async function getLicenseDetails(licenseKey) {
  const res = await fetch(`${BASE}/getLicense`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      licenseKey,
      adminSecret: process.env.CG_LICENSE_ADMIN_SECRET,
    }),
  });
  return res.json();
  // Returns: { license, activations: [{ fingerprint, hostname, lastSeen }], violations }
}

Display per license: key (truncated + copy), status badge, activation count (e.g. "2/5 machines").

Step 6: Creator Admin UI

For the /dashboard/licenses panel:

// List all licenses for a product
async function getProductLicenses(productId) {
  const res = await fetch(`${BASE}/listLicenses`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      productId,
      adminSecret: process.env.CG_LICENSE_ADMIN_SECRET,
    }),
  });
  return res.json();
}

// Revoke a license (refund, abuse, request)
async function revokeLicense(licenseKey, reason) {
  await fetch(`${BASE}/revokeLicense`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      licenseKey,
      reason,
      adminSecret: process.env.CG_LICENSE_ADMIN_SECRET,
    }),
  });
}

// Resolve false-positive violations
async function resolveViolations(licenseKey, violationIds) {
  await fetch(`${BASE}/resolveLicense`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      licenseKey,
      violationIds,
      adminSecret: process.env.CG_LICENSE_ADMIN_SECRET,
    }),
  });
}

Part 4: Testing Checklist

Run these curl commands against your deployed instance before going live.

Create a Test Product

curl -X POST https://YOUR-URL/createProduct \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test Plugin",
    "slug": "test-plugin",
    "creatorId": "creator-test-01",
    "adminSecret": "YOUR_ADMIN_SECRET"
  }'

Create a Variant

curl -X POST https://YOUR-URL/createVariant \
  -H "Content-Type: application/json" \
  -d '{
    "productId": "test-plugin",
    "name": "indie",
    "licenseType": "per-machine",
    "maxMachines": 2,
    "adminSecret": "YOUR_ADMIN_SECRET"
  }'

Create a License

curl -X POST https://YOUR-URL/createLicense \
  -H "Content-Type: application/json" \
  -d '{
    "email": "test@example.com",
    "productId": "test-plugin",
    "variant": "indie",
    "adminSecret": "YOUR_ADMIN_SECRET"
  }'

Test Activation

curl -X POST https://YOUR-URL/activate \
  -H "Content-Type: application/json" \
  -d '{
    "licenseKey": "XXXX-XXXX-XXXX-XXXX-XXXX",
    "machineFingerprint": "test-fingerprint-001",
    "hostname": "dev-machine",
    "productId": "test-plugin"
  }'

Pre-Launch Checklist

  • [ ] Firebase config secrets are set and verified with functions:config:get
  • [ ] Functions and Firestore indexes are deployed
  • [ ] Test product and variant created successfully
  • [ ] Manual license creation returns a valid key
  • [ ] Activation returns a signed JWT
  • [ ] Validation/refresh flow works (call /validate with the JWT)
  • [ ] cglounge webhook purchase event creates a license
  • [ ] cglounge webhook refund event revokes the license
  • [ ] Stripe webhook endpoint registered in Stripe Dashboard
  • [ ] Duplicate purchaseId returns 409 (dedup working)

Part 5: Next Steps

  1. Generate admin.secret and cglounge.webhook_secret using openssl rand -hex 32
  2. Configure Firebase:
    firebase functions:config:set admin.secret="..." cglounge.webhook_secret="..."
    
  3. Register Stripe webhook endpoint, copy whsec_... secret, configure:
    firebase functions:config:set stripe.webhook_secret="whsec_..."
    
  4. Deploy:
    firebase deploy --only functions,firestore:indexes
    
  5. Add CG_LICENSE_SERVER_URL, CG_LICENSE_ADMIN_SECRET, and CG_LICENSE_WEBHOOK_SECRET to cglounge backend env
  6. Implement webhook calls in cglounge: purchase, refund, dispute
  7. Build user license management UI (/account/licenses)
  8. Build creator admin panel (/dashboard/licenses)
  9. Run the testing checklist end to end
  10. Create a real product via the creator dashboard and do a test purchase