Skip to main content

Content Licensing Flow

Purpose: How buyers discover, purchase, and use licensed content from creators.

Purchase Flow Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                      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

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.
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

// 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

// 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

// 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

// 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

// 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

// 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

// Unlimited uses, buyer only
const unlimitedLicense = {
  type: 'unlimited',
  usage_limit: null,
  valid_for: 'perpetual',
  restrictions: [
    'Buyer account only',
    'Modifications allowed',
    'No resale'
  ]
};

Exclusive

// 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

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

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

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

DocumentWhat It Covers
uci-system.mdOverall UCI architecture
iris-verification.mdCreator verification
payouts.mdCreator payments
SCHEMA.mdTable definitions