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¶
Both produce a 64-character hex string suitable for any of the three secrets.
Getting the Stripe Webhook Secret¶
- Go to Stripe Dashboard > Developers > Webhooks
- Click Add endpoint
- Set the endpoint URL:
- Select the following events:
checkout.session.completedcharge.refundedcharge.dispute.createdinvoice.paidcustomer.subscription.deleted- Save the endpoint, then click Reveal under Signing secret
- Copy the
whsec_...value. That is yourstripe.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¶
You can split this into two steps if needed:
Endpoint URLs After Deploy¶
All endpoints live under:
| 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
/validatewith the JWT) - [ ] cglounge webhook purchase event creates a license
- [ ] cglounge webhook refund event revokes the license
- [ ] Stripe webhook endpoint registered in Stripe Dashboard
- [ ] Duplicate
purchaseIdreturns 409 (dedup working)
Part 5: Next Steps¶
- Generate
admin.secretandcglounge.webhook_secretusingopenssl rand -hex 32 - Configure Firebase:
- Register Stripe webhook endpoint, copy
whsec_...secret, configure: - Deploy:
- Add
CG_LICENSE_SERVER_URL,CG_LICENSE_ADMIN_SECRET, andCG_LICENSE_WEBHOOK_SECRETto cglounge backend env - Implement webhook calls in cglounge: purchase, refund, dispute
- Build user license management UI (
/account/licenses) - Build creator admin panel (
/dashboard/licenses) - Run the testing checklist end to end
- Create a real product via the creator dashboard and do a test purchase