Content Licensing Flow
Purpose: How buyers discover, purchase, and use licensed content from creators.Purchase Flow Overview
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ LICENSING PURCHASE FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ BUYER PLATFORM CREATOR │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Search │ │ Returns │ │ │ │
│ │ Content │────Query──────▶│ Results │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │ │ │
│ ┌─────────┐ ┌─────────┐ │ │
│ │ Select │ │ Shows │ │ │
│ │ License │────Details────▶│ Terms │ │ │
│ │ Type │ │ & Price │ │ │
│ └─────────┘ └─────────┘ │ │
│ │ │ │ │
│ ┌─────────┐ ┌─────────┐ │ │
│ │ Pay via │ │ Process │ │ │
│ │ Stripe │────Payment────▶│ Payment │ │ │
│ └─────────┘ └─────────┘ │ │
│ │ │ │ │
│ │ ┌─────────┐ ┌─────────┐ │
│ │ │ Create │ │ Receive │ │
│ │ │ Purchase│───Notification─▶│ Alert │ │
│ │ │ Record │ └─────────┘ │
│ │ └─────────┘ │ │
│ │ │ │ │
│ ┌─────────┐ ┌─────────┐ │ │
│ │ Access │ │ Deliver │ │ │
│ │ Content │◀───Download────│ Asset │ │ │
│ └─────────┘ └─────────┘ │ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Database Tables
creator_license_purchases
Copy
CREATE TABLE creator_license_purchases (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
canonical_id text UNIQUE NOT NULL,
-- References
license_id uuid REFERENCES creator_licenses(id),
buyer_id uuid REFERENCES users(id),
-- License snapshot (in case terms change)
license_type text NOT NULL,
content_url text NOT NULL,
-- Pricing breakdown
purchase_price numeric NOT NULL,
platform_fee numeric NOT NULL, -- Our cut
creator_payout numeric NOT NULL, -- Creator's cut
-- Usage tracking
usage_limit integer, -- Copied from license
usage_count integer DEFAULT 0,
-- Usage context (where it's being used)
usage_context text, -- 'website', 'social', 'advertising', 'internal'
usage_notes text,
-- Payment
stripe_payment_intent_id text,
stripe_charge_id text,
payment_status text DEFAULT 'pending', -- 'pending', 'paid', 'failed', 'refunded'
paid_at timestamptz,
-- Download tracking
download_count integer DEFAULT 0,
last_downloaded_at timestamptz,
-- Validity
valid_from timestamptz DEFAULT now(),
valid_until timestamptz, -- For time-limited licenses
is_active boolean DEFAULT true,
created_at timestamptz DEFAULT now(),
updated_at timestamptz DEFAULT now()
);
CREATE INDEX idx_purchases_license ON creator_license_purchases(license_id);
CREATE INDEX idx_purchases_buyer ON creator_license_purchases(buyer_id);
CREATE INDEX idx_purchases_status ON creator_license_purchases(payment_status);
CREATE INDEX idx_purchases_active ON creator_license_purchases(is_active) WHERE is_active = true;
license_usage_log
Track every use of licensed content.Copy
CREATE TABLE license_usage_log (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
purchase_id uuid REFERENCES creator_license_purchases(id),
-- Usage details
usage_type text NOT NULL, -- 'download', 'embed', 'api_access'
usage_platform text, -- 'website', 'instagram', 'tiktok'
usage_url text, -- Where it was used
-- Technical
ip_address inet,
user_agent text,
created_at timestamptz DEFAULT now()
);
CREATE INDEX idx_usage_log_purchase ON license_usage_log(purchase_id);
CREATE INDEX idx_usage_log_time ON license_usage_log(created_at DESC);
Implementation
1. Search Licenses
Copy
// API: GET /api/licenses/search
export async function searchLicenses(params) {
const {
query,
content_type,
tags,
min_price,
max_price,
license_type,
verified_only = true
} = params;
let queryBuilder = supabase
.from('creator_licenses')
.select(`
*,
creator:creator_profiles(
id,
display_name,
avatar_url,
iris_verified,
verification_level
)
`)
.eq('is_active', true);
// Filters
if (content_type) {
queryBuilder = queryBuilder.eq('content_type', content_type);
}
if (tags?.length) {
queryBuilder = queryBuilder.overlaps('tags', tags);
}
if (min_price) {
queryBuilder = queryBuilder.gte('price_per_use', min_price);
}
if (max_price) {
queryBuilder = queryBuilder.lte('price_per_use', max_price);
}
if (license_type) {
queryBuilder = queryBuilder.eq('license_type', license_type);
}
if (verified_only) {
queryBuilder = queryBuilder.eq('creator.iris_verified', true);
}
// Text search on title/description
if (query) {
queryBuilder = queryBuilder.or(
`title.ilike.%${query}%,description.ilike.%${query}%`
);
}
const { data, error } = await queryBuilder
.order('usage_count', { ascending: false })
.limit(20);
return { data, error };
}
2. Get License Details
Copy
// API: GET /api/licenses/:id
export async function getLicenseDetails(licenseId) {
const { data: license } = await supabase
.from('creator_licenses')
.select(`
*,
creator:creator_profiles(
id,
display_name,
bio,Do you want me to restart it?
avatar_url,
iris_verified,
total_licenses,
total_revenue
)
`)
.eq('id', licenseId)
.single();
// Get similar licenses
const { data: similar } = await supabase
.from('creator_licenses')
.select('id, title, thumbnail_url, price_per_use')
.eq('content_type', license.content_type)
.neq('id', licenseId)
.limit(4);
return { license, similar };
}
3. Create Purchase
Copy
// API: POST /api/licenses/:id/purchase
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function createPurchase(licenseId, buyerId, licenseType) {
// 1. Get license and verify availability
const { data: license } = await supabase
.from('creator_licenses')
.select('*, creator:creator_profiles(*)')
.eq('id', licenseId)
.single();
if (!license.is_active) {
throw new Error('License no longer available');
}
// Check usage limits for single_use
if (license.license_type === 'single_use' &&
license.usage_limit &&
license.usage_count >= license.usage_limit) {
throw new Error('License usage limit reached');
}
// Check exclusivity
if (license.is_exclusive) {
throw new Error('This content has been exclusively licensed');
}
// 2. Calculate pricing
const pricing = calculatePricing(license, licenseType);
// 3. Create Stripe payment intent
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(pricing.total * 100), // Stripe uses cents
currency: 'usd',
metadata: {
license_id: licenseId,
buyer_id: buyerId,
license_type: licenseType
}
});
// 4. Create purchase record (pending)
const { data: purchase } = await supabase
.from('creator_license_purchases')
.insert({
canonical_id: `PUR-${Date.now()}`,
license_id: licenseId,
buyer_id: buyerId,
license_type: licenseType,
content_url: license.content_url,
purchase_price: pricing.total,
platform_fee: pricing.platformFee,
creator_payout: pricing.creatorPayout,
usage_limit: license.usage_limit,
stripe_payment_intent_id: paymentIntent.id,
payment_status: 'pending'
})
.select()
.single();
return {
purchase,
clientSecret: paymentIntent.client_secret
};
}
function calculatePricing(license, licenseType) {
let basePrice;
switch (licenseType) {
case 'single_use':
basePrice = license.price_per_use;
break;
case 'unlimited':
basePrice = license.price_unlimited;
break;
case 'exclusive':
basePrice = license.price_exclusive;
break;
default:
throw new Error('Invalid license type');
}
// Calculate split based on creator verification level
const feeRate = license.creator.iris_verified ? 0.10 : 0.15;
const platformFee = basePrice * feeRate;
const creatorPayout = basePrice - platformFee;
return {
total: basePrice,
platformFee,
creatorPayout,
feeRate
};
}
4. Handle Payment Webhook
Copy
// API: POST /api/webhooks/stripe
export async function handleStripeWebhook(event) {
switch (event.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(event.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailure(event.data.object);
break;
}
}
async function handlePaymentSuccess(paymentIntent) {
const { license_id, buyer_id } = paymentIntent.metadata;
// 1. Update purchase record
const { data: purchase } = await supabase
.from('creator_license_purchases')
.update({
payment_status: 'paid',
paid_at: new Date(),
stripe_charge_id: paymentIntent.latest_charge
})
.eq('stripe_payment_intent_id', paymentIntent.id)
.select()
.single();
// 2. Increment license usage count
await supabase.rpc('increment_license_usage', {
license_id
});
// 3. Update creator stats
await supabase.rpc('add_creator_revenue', {
creator_id: purchase.creator_id,
amount: purchase.creator_payout
});
// 4. Queue payout (handled by payouts system)
await supabase
.from('pending_payouts')
.insert({
creator_id: purchase.creator_id,
purchase_id: purchase.id,
amount: purchase.creator_payout
});
// 5. Notify creator
await sendNotification({
user_id: purchase.creator_id,
type: 'license_purchased',
data: {
license_id,
amount: purchase.creator_payout,
buyer_name: 'Anonymous' // Privacy
}
});
// 6. If exclusive, mark license as exclusive
if (purchase.license_type === 'exclusive') {
await supabase
.from('creator_licenses')
.update({
is_exclusive: true,
exclusive_buyer_id: buyer_id,
exclusive_until: null // Permanent unless specified
})
.eq('id', license_id);
}
}
5. Download Content
Copy
// API: GET /api/purchases/:id/download
export async function downloadContent(purchaseId, userId) {
// 1. Verify ownership and validity
const { data: purchase } = await supabase
.from('creator_license_purchases')
.select('*')
.eq('id', purchaseId)
.eq('buyer_id', userId)
.eq('payment_status', 'paid')
.single();
if (!purchase) {
throw new Error('Purchase not found or not paid');
}
if (!purchase.is_active) {
throw new Error('License is no longer active');
}
// Check expiration
if (purchase.valid_until && new Date(purchase.valid_until) < new Date()) {
throw new Error('License has expired');
}
// Check usage limit
if (purchase.usage_limit && purchase.usage_count >= purchase.usage_limit) {
throw new Error('Download limit reached');
}
// 2. Log usage
await supabase
.from('license_usage_log')
.insert({
purchase_id: purchaseId,
usage_type: 'download',
ip_address: getClientIP(),
user_agent: getClientUserAgent()
});
// 3. Increment download count
await supabase
.from('creator_license_purchases')
.update({
download_count: purchase.download_count + 1,
usage_count: purchase.usage_count + 1,
last_downloaded_at: new Date()
})
.eq('id', purchaseId);
// 4. Generate signed download URL (expires in 1 hour)
const signedUrl = await generateSignedUrl(purchase.content_url, 3600);
return { downloadUrl: signedUrl };
}
License Types Deep Dive
Single Use
Copy
// One download, one platform
const singleUseLicense = {
type: 'single_use',
usage_limit: 1,
valid_for: '1 year',
restrictions: [
'One platform only',
'No modifications',
'Attribution required'
]
};
Unlimited
Copy
// Unlimited uses, buyer only
const unlimitedLicense = {
type: 'unlimited',
usage_limit: null,
valid_for: 'perpetual',
restrictions: [
'Buyer account only',
'Modifications allowed',
'No resale'
]
};
Exclusive
Copy
// Full rights transfer
const exclusiveLicense = {
type: 'exclusive',
usage_limit: null,
valid_for: 'perpetual',
restrictions: [
'Full ownership transfer',
'Content removed from marketplace',
'Can resell/sublicense'
],
onPurchase: async (licenseId, buyerId) => {
// Remove from marketplace
await supabase
.from('creator_licenses')
.update({
is_active: false,
is_exclusive: true,
exclusive_buyer_id: buyerId
})
.eq('id', licenseId);
}
};
Buyer Dashboard
Copy
-- Get buyer's purchases
SELECT
p.id,
p.license_type,
p.purchase_price,
p.usage_count,
p.usage_limit,
p.paid_at,
p.valid_until,
l.title,
l.content_type,
l.thumbnail_url,
c.display_name as creator_name
FROM creator_license_purchases p
JOIN creator_licenses l ON p.license_id = l.id
JOIN creator_profiles c ON l.creator_id = c.id
WHERE p.buyer_id = :buyer_id
AND p.payment_status = 'paid'
ORDER BY p.paid_at DESC;
Creator Dashboard
Copy
-- Get creator's sales
SELECT
p.id,
p.license_type,
p.creator_payout,
p.paid_at,
l.title,
l.content_type
FROM creator_license_purchases p
JOIN creator_licenses l ON p.license_id = l.id
WHERE l.creator_id = :creator_id
AND p.payment_status = 'paid'
ORDER BY p.paid_at DESC;
-- Sales summary
SELECT
COUNT(*) as total_sales,
SUM(creator_payout) as total_earnings,
AVG(creator_payout) as avg_sale,
COUNT(DISTINCT buyer_id) as unique_buyers
FROM creator_license_purchases p
JOIN creator_licenses l ON p.license_id = l.id
WHERE l.creator_id = :creator_id
AND p.payment_status = 'paid';
Refunds
Copy
async function processRefund(purchaseId, reason) {
const { data: purchase } = await supabase
.from('creator_license_purchases')
.select('*')
.eq('id', purchaseId)
.single();
// Only allow refunds within 7 days, no downloads
if (purchase.download_count > 0) {
throw new Error('Cannot refund after download');
}
const daysSincePurchase =
(Date.now() - new Date(purchase.paid_at)) / (1000 * 60 * 60 * 24);
if (daysSincePurchase > 7) {
throw new Error('Refund window expired (7 days)');
}
// Process Stripe refund
await stripe.refunds.create({
payment_intent: purchase.stripe_payment_intent_id,
reason: 'requested_by_customer'
});
// Update purchase
await supabase
.from('creator_license_purchases')
.update({
payment_status: 'refunded',
is_active: false
})
.eq('id', purchaseId);
// Reverse creator stats
await supabase.rpc('subtract_creator_revenue', {
creator_id: purchase.creator_id,
amount: purchase.creator_payout
});
// Remove from pending payouts
await supabase
.from('pending_payouts')
.delete()
.eq('purchase_id', purchaseId);
}
Related Documentation
| Document | What It Covers |
|---|---|
| uci-system.md | Overall UCI architecture |
| iris-verification.md | Creator verification |
| payouts.md | Creator payments |
| SCHEMA.md | Table definitions |