# Digital Product Delivery

**Meta-Description:** Digitale Produkte sicher ausliefern. Download-Links, License Keys, E-Books und Software mit Node.js und Next.js automatisiert bereitstellen.

**Keywords:** Digital Products, Download Links, License Keys, E-Book Delivery, Software Distribution, Signed URLs, Content Protection

---

## Einführung

**Digital Product Delivery** automatisiert die Auslieferung von Downloads, Lizenzen und digitalen Gütern nach dem Kauf. Von **signierten URLs** bis **License Key Generation** – sichere Delivery ist entscheidend für digitale Geschäftsmodelle. Dieser Guide zeigt Production-ready Implementierungen.

---

## Delivery Architecture

```
┌─────────────────────────────────────────────────────────────┐
│           DIGITAL PRODUCT DELIVERY SYSTEM                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                 PRODUCT TYPES                        │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │                                                     │   │
│  │  📁 Downloadable Files    📋 License Keys          │   │
│  │  ├── E-Books (PDF/EPUB)   ├── Software Licenses    │   │
│  │  ├── Audio/Video          ├── API Access Keys      │   │
│  │  ├── Software Packages    └── Subscription Codes   │   │
│  │  └── Templates/Assets                              │   │
│  │                                                     │   │
│  │  🔗 Access Grants          📧 Email Delivery       │   │
│  │  ├── Course Access         ├── Welcome Emails      │   │
│  │  ├── Membership Areas      ├── Download Links      │   │
│  │  └── Private Content       └── License Delivery    │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                          │                                  │
│                          ▼                                  │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              DELIVERY PIPELINE                       │   │
│  │                                                     │   │
│  │  Payment ──► Fulfillment ──► Delivery ──► Access   │   │
│  │     │            │              │            │      │   │
│  │  Webhook    Generate      Send Email    Track      │   │
│  │  Received   Assets/Keys   with Links    Downloads  │   │
│  │                                                     │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Security Measures:                                        │
│  ├── Signed URLs with expiration                          │
│  ├── Download limits per purchase                         │
│  ├── IP tracking and fraud detection                      │
│  └── License key validation                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

---

## Product & License Models

```typescript
// prisma/schema.prisma
model DigitalProduct {
  id            String   @id @default(cuid())
  name          String
  slug          String   @unique
  description   String?
  type          ProductType
  price         Int      // cents
  currency      String   @default("EUR")

  // File-based products
  fileKey       String?  // S3 key
  fileName      String?
  fileSize      Int?
  fileMimeType  String?

  // License-based products
  licenseType   LicenseType?
  maxActivations Int?

  // Metadata
  metadata      Json?
  active        Boolean  @default(true)
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  purchases     Purchase[]
  licenses      License[]
}

model Purchase {
  id            String   @id @default(cuid())
  userId        String
  productId     String
  product       DigitalProduct @relation(fields: [productId], references: [id])

  // Payment info
  stripePaymentId String?
  amount        Int
  currency      String
  status        PurchaseStatus @default(PENDING)

  // Delivery tracking
  deliveredAt   DateTime?
  downloadCount Int      @default(0)
  maxDownloads  Int      @default(5)

  // Download links
  downloads     Download[]
  licenses      License[]

  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt

  user          User     @relation(fields: [userId], references: [id])
}

model Download {
  id          String   @id @default(cuid())
  purchaseId  String
  purchase    Purchase @relation(fields: [purchaseId], references: [id])

  token       String   @unique
  expiresAt   DateTime
  downloadedAt DateTime?
  ipAddress   String?
  userAgent   String?

  createdAt   DateTime @default(now())
}

model License {
  id            String   @id @default(cuid())
  purchaseId    String
  purchase      Purchase @relation(fields: [purchaseId], references: [id])
  productId     String
  product       DigitalProduct @relation(fields: [productId], references: [id])

  key           String   @unique
  status        LicenseStatus @default(ACTIVE)
  activations   Int      @default(0)
  maxActivations Int

  // Activation tracking
  activationRecords LicenseActivation[]

  expiresAt     DateTime?
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

model LicenseActivation {
  id          String   @id @default(cuid())
  licenseId   String
  license     License  @relation(fields: [licenseId], references: [id])

  machineId   String
  machineName String?
  ipAddress   String?
  activatedAt DateTime @default(now())
  deactivatedAt DateTime?

  @@unique([licenseId, machineId])
}

enum ProductType {
  DOWNLOAD      // Downloadable file
  LICENSE       // Software license
  SUBSCRIPTION  // Recurring access
  COURSE        // Online course access
  MEMBERSHIP    // Membership access
}

enum LicenseType {
  PERPETUAL     // Forever
  SUBSCRIPTION  // Time-limited
  TRIAL         // Trial period
}

enum PurchaseStatus {
  PENDING
  COMPLETED
  REFUNDED
  FAILED
}

enum LicenseStatus {
  ACTIVE
  SUSPENDED
  REVOKED
  EXPIRED
}
```

---

## Signed URL Generation

```typescript
// lib/delivery/signed-urls.ts
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import crypto from 'crypto';

const s3 = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
  }
});

interface SignedUrlOptions {
  expiresIn?: number; // seconds
  downloadFilename?: string;
}

export async function generateSignedDownloadUrl(
  fileKey: string,
  options: SignedUrlOptions = {}
): Promise<string> {
  const { expiresIn = 3600, downloadFilename } = options;

  const command = new GetObjectCommand({
    Bucket: process.env.S3_BUCKET!,
    Key: fileKey,
    ResponseContentDisposition: downloadFilename
      ? `attachment; filename="${downloadFilename}"`
      : undefined
  });

  return getSignedUrl(s3, command, { expiresIn });
}

// Custom signed URL for self-hosted files
export function generateCustomSignedUrl(
  path: string,
  expiresIn: number = 3600
): string {
  const expires = Math.floor(Date.now() / 1000) + expiresIn;
  const signature = crypto
    .createHmac('sha256', process.env.DOWNLOAD_SECRET!)
    .update(`${path}:${expires}`)
    .digest('hex');

  const baseUrl = process.env.NEXT_PUBLIC_APP_URL;
  return `${baseUrl}/api/download/${path}?expires=${expires}&sig=${signature}`;
}

export function verifySignedUrl(
  path: string,
  expires: string,
  signature: string
): boolean {
  const expiresNum = parseInt(expires, 10);

  // Check expiration
  if (Date.now() / 1000 > expiresNum) {
    return false;
  }

  // Verify signature
  const expectedSig = crypto
    .createHmac('sha256', process.env.DOWNLOAD_SECRET!)
    .update(`${path}:${expiresNum}`)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSig)
  );
}
```

---

## Download Token System

```typescript
// lib/delivery/download-tokens.ts
import { db } from '@/lib/db';
import crypto from 'crypto';
import { addHours } from 'date-fns';

interface CreateDownloadTokenResult {
  token: string;
  downloadUrl: string;
  expiresAt: Date;
}

export async function createDownloadToken(
  purchaseId: string,
  expirationHours: number = 24
): Promise<CreateDownloadTokenResult> {
  const purchase = await db.purchase.findUnique({
    where: { id: purchaseId },
    include: { product: true }
  });

  if (!purchase) {
    throw new Error('Purchase not found');
  }

  if (purchase.downloadCount >= purchase.maxDownloads) {
    throw new Error('Download limit exceeded');
  }

  const token = crypto.randomBytes(32).toString('hex');
  const expiresAt = addHours(new Date(), expirationHours);

  await db.download.create({
    data: {
      purchaseId,
      token,
      expiresAt
    }
  });

  const downloadUrl = `${process.env.NEXT_PUBLIC_APP_URL}/api/download/${token}`;

  return { token, downloadUrl, expiresAt };
}

export async function validateAndConsumeToken(
  token: string,
  ipAddress?: string,
  userAgent?: string
): Promise<{ fileKey: string; fileName: string } | null> {
  const download = await db.download.findUnique({
    where: { token },
    include: {
      purchase: {
        include: { product: true }
      }
    }
  });

  if (!download) {
    return null;
  }

  // Check expiration
  if (download.expiresAt < new Date()) {
    return null;
  }

  // Check if already used
  if (download.downloadedAt) {
    return null;
  }

  // Check purchase download limits
  if (download.purchase.downloadCount >= download.purchase.maxDownloads) {
    return null;
  }

  // Mark as used and increment counter
  await db.$transaction([
    db.download.update({
      where: { id: download.id },
      data: {
        downloadedAt: new Date(),
        ipAddress,
        userAgent
      }
    }),
    db.purchase.update({
      where: { id: download.purchaseId },
      data: {
        downloadCount: { increment: 1 }
      }
    })
  ]);

  return {
    fileKey: download.purchase.product.fileKey!,
    fileName: download.purchase.product.fileName!
  };
}
```

---

## License Key Generation

```typescript
// lib/delivery/license-keys.ts
import crypto from 'crypto';
import { db } from '@/lib/db';

interface LicenseKeyOptions {
  prefix?: string;
  segments?: number;
  segmentLength?: number;
}

export function generateLicenseKey(options: LicenseKeyOptions = {}): string {
  const {
    prefix = 'PRO',
    segments = 4,
    segmentLength = 5
  } = options;

  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // No I, O, 0, 1
  const keySegments: string[] = [];

  for (let i = 0; i < segments; i++) {
    let segment = '';
    for (let j = 0; j < segmentLength; j++) {
      const randomIndex = crypto.randomInt(0, chars.length);
      segment += chars[randomIndex];
    }
    keySegments.push(segment);
  }

  return `${prefix}-${keySegments.join('-')}`;
}

// Checksum-validated license key
export function generateValidatedLicenseKey(productCode: string): string {
  const key = generateLicenseKey({ prefix: productCode });
  const checksum = calculateChecksum(key);
  return `${key}-${checksum}`;
}

function calculateChecksum(key: string): string {
  const hash = crypto
    .createHash('sha256')
    .update(key + process.env.LICENSE_SECRET!)
    .digest('hex');
  return hash.substring(0, 4).toUpperCase();
}

export function validateLicenseKeyFormat(key: string): boolean {
  // Format: XXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXX
  const pattern = /^[A-Z]{2,4}-([A-Z0-9]{5}-){3,4}[A-Z0-9]{4}$/;

  if (!pattern.test(key)) {
    return false;
  }

  // Validate checksum
  const parts = key.split('-');
  const checksum = parts.pop()!;
  const keyWithoutChecksum = parts.join('-');

  return calculateChecksum(keyWithoutChecksum) === checksum;
}

// Create license in database
export async function createLicense(
  purchaseId: string,
  productId: string
): Promise<License> {
  const product = await db.digitalProduct.findUnique({
    where: { id: productId }
  });

  if (!product) {
    throw new Error('Product not found');
  }

  const key = generateValidatedLicenseKey(
    product.slug.substring(0, 3).toUpperCase()
  );

  return db.license.create({
    data: {
      purchaseId,
      productId,
      key,
      maxActivations: product.maxActivations || 3,
      expiresAt: product.licenseType === 'SUBSCRIPTION'
        ? new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) // 1 year
        : null
    }
  });
}
```

---

## License Activation API

```typescript
// app/api/licenses/activate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
import { validateLicenseKeyFormat } from '@/lib/delivery/license-keys';

interface ActivationRequest {
  licenseKey: string;
  machineId: string;
  machineName?: string;
}

export async function POST(request: NextRequest) {
  try {
    const body: ActivationRequest = await request.json();
    const { licenseKey, machineId, machineName } = body;

    // Validate format
    if (!validateLicenseKeyFormat(licenseKey)) {
      return NextResponse.json(
        { error: 'Invalid license key format' },
        { status: 400 }
      );
    }

    // Find license
    const license = await db.license.findUnique({
      where: { key: licenseKey },
      include: {
        product: true,
        activationRecords: true
      }
    });

    if (!license) {
      return NextResponse.json(
        { error: 'License not found' },
        { status: 404 }
      );
    }

    // Check status
    if (license.status !== 'ACTIVE') {
      return NextResponse.json(
        { error: `License is ${license.status.toLowerCase()}` },
        { status: 403 }
      );
    }

    // Check expiration
    if (license.expiresAt && license.expiresAt < new Date()) {
      await db.license.update({
        where: { id: license.id },
        data: { status: 'EXPIRED' }
      });

      return NextResponse.json(
        { error: 'License has expired' },
        { status: 403 }
      );
    }

    // Check if already activated on this machine
    const existingActivation = license.activationRecords.find(
      a => a.machineId === machineId && !a.deactivatedAt
    );

    if (existingActivation) {
      return NextResponse.json({
        success: true,
        message: 'Already activated on this machine',
        license: formatLicenseResponse(license)
      });
    }

    // Check activation limit
    const activeActivations = license.activationRecords.filter(
      a => !a.deactivatedAt
    ).length;

    if (activeActivations >= license.maxActivations) {
      return NextResponse.json(
        {
          error: 'Maximum activations reached',
          maxActivations: license.maxActivations,
          currentActivations: activeActivations
        },
        { status: 403 }
      );
    }

    // Create activation
    const ipAddress = request.headers.get('x-forwarded-for') ||
                      request.headers.get('x-real-ip');

    await db.$transaction([
      db.licenseActivation.create({
        data: {
          licenseId: license.id,
          machineId,
          machineName,
          ipAddress
        }
      }),
      db.license.update({
        where: { id: license.id },
        data: { activations: { increment: 1 } }
      })
    ]);

    return NextResponse.json({
      success: true,
      message: 'License activated successfully',
      license: formatLicenseResponse(license)
    });

  } catch (error) {
    console.error('Activation error:', error);
    return NextResponse.json(
      { error: 'Activation failed' },
      { status: 500 }
    );
  }
}

function formatLicenseResponse(license: any) {
  return {
    productName: license.product.name,
    licenseType: license.product.licenseType,
    expiresAt: license.expiresAt,
    features: license.product.metadata?.features || []
  };
}

// Deactivation endpoint
export async function DELETE(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const licenseKey = searchParams.get('key');
  const machineId = searchParams.get('machineId');

  if (!licenseKey || !machineId) {
    return NextResponse.json(
      { error: 'License key and machine ID required' },
      { status: 400 }
    );
  }

  const license = await db.license.findUnique({
    where: { key: licenseKey }
  });

  if (!license) {
    return NextResponse.json(
      { error: 'License not found' },
      { status: 404 }
    );
  }

  const activation = await db.licenseActivation.findUnique({
    where: {
      licenseId_machineId: {
        licenseId: license.id,
        machineId
      }
    }
  });

  if (!activation || activation.deactivatedAt) {
    return NextResponse.json(
      { error: 'No active activation found' },
      { status: 404 }
    );
  }

  await db.$transaction([
    db.licenseActivation.update({
      where: { id: activation.id },
      data: { deactivatedAt: new Date() }
    }),
    db.license.update({
      where: { id: license.id },
      data: { activations: { decrement: 1 } }
    })
  ]);

  return NextResponse.json({
    success: true,
    message: 'License deactivated successfully'
  });
}
```

---

## Fulfillment Service

```typescript
// lib/delivery/fulfillment.ts
import { db } from '@/lib/db';
import { createDownloadToken } from './download-tokens';
import { createLicense } from './license-keys';
import { sendEmail } from '@/lib/email';

interface FulfillmentResult {
  success: boolean;
  downloadUrl?: string;
  licenseKey?: string;
  error?: string;
}

export async function fulfillPurchase(
  purchaseId: string
): Promise<FulfillmentResult> {
  const purchase = await db.purchase.findUnique({
    where: { id: purchaseId },
    include: {
      product: true,
      user: true
    }
  });

  if (!purchase) {
    return { success: false, error: 'Purchase not found' };
  }

  if (purchase.status !== 'COMPLETED') {
    return { success: false, error: 'Purchase not completed' };
  }

  if (purchase.deliveredAt) {
    return { success: false, error: 'Already delivered' };
  }

  try {
    let downloadUrl: string | undefined;
    let licenseKey: string | undefined;

    const { product } = purchase;

    // Handle different product types
    switch (product.type) {
      case 'DOWNLOAD':
        const downloadResult = await createDownloadToken(purchaseId);
        downloadUrl = downloadResult.downloadUrl;
        break;

      case 'LICENSE':
        const license = await createLicense(purchaseId, product.id);
        licenseKey = license.key;
        break;

      case 'SUBSCRIPTION':
      case 'MEMBERSHIP':
        // Grant access in database
        await grantAccess(purchase.userId, product.id);
        break;

      case 'COURSE':
        await enrollInCourse(purchase.userId, product.id);
        break;
    }

    // Mark as delivered
    await db.purchase.update({
      where: { id: purchaseId },
      data: { deliveredAt: new Date() }
    });

    // Send delivery email
    await sendDeliveryEmail(purchase, downloadUrl, licenseKey);

    return { success: true, downloadUrl, licenseKey };

  } catch (error) {
    console.error('Fulfillment error:', error);
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Fulfillment failed'
    };
  }
}

async function grantAccess(userId: string, productId: string): Promise<void> {
  await db.userAccess.create({
    data: {
      userId,
      productId,
      grantedAt: new Date()
    }
  });
}

async function enrollInCourse(userId: string, productId: string): Promise<void> {
  const product = await db.digitalProduct.findUnique({
    where: { id: productId }
  });

  const courseId = product?.metadata?.courseId;
  if (!courseId) {
    throw new Error('Course ID not found in product metadata');
  }

  await db.courseEnrollment.create({
    data: {
      userId,
      courseId,
      enrolledAt: new Date()
    }
  });
}

async function sendDeliveryEmail(
  purchase: any,
  downloadUrl?: string,
  licenseKey?: string
): Promise<void> {
  const { user, product } = purchase;

  await sendEmail({
    to: user.email,
    template: 'product-delivery',
    data: {
      userName: user.name,
      productName: product.name,
      productType: product.type,
      downloadUrl,
      licenseKey,
      supportEmail: 'support@example.com'
    }
  });
}
```

---

## Download API Endpoint

```typescript
// app/api/download/[token]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { validateAndConsumeToken } from '@/lib/delivery/download-tokens';

const s3 = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
  }
});

export async function GET(
  request: NextRequest,
  { params }: { params: { token: string } }
) {
  const { token } = params;

  const ipAddress = request.headers.get('x-forwarded-for') ||
                    request.headers.get('x-real-ip') ||
                    undefined;
  const userAgent = request.headers.get('user-agent') || undefined;

  // Validate and consume token
  const result = await validateAndConsumeToken(token, ipAddress, userAgent);

  if (!result) {
    return NextResponse.json(
      { error: 'Invalid or expired download link' },
      { status: 403 }
    );
  }

  try {
    // Fetch file from S3
    const command = new GetObjectCommand({
      Bucket: process.env.S3_BUCKET!,
      Key: result.fileKey
    });

    const s3Response = await s3.send(command);

    if (!s3Response.Body) {
      throw new Error('File not found');
    }

    // Stream the file
    const stream = s3Response.Body as ReadableStream;

    return new NextResponse(stream, {
      headers: {
        'Content-Type': s3Response.ContentType || 'application/octet-stream',
        'Content-Disposition': `attachment; filename="${result.fileName}"`,
        'Content-Length': s3Response.ContentLength?.toString() || '',
        'Cache-Control': 'no-store'
      }
    });

  } catch (error) {
    console.error('Download error:', error);
    return NextResponse.json(
      { error: 'Download failed' },
      { status: 500 }
    );
  }
}
```

---

## Stripe Webhook Integration

```typescript
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { db } from '@/lib/db';
import { fulfillPurchase } from '@/lib/delivery/fulfillment';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid signature' },
      { status: 400 }
    );
  }

  switch (event.type) {
    case 'checkout.session.completed':
      await handleCheckoutComplete(event.data.object as Stripe.Checkout.Session);
      break;

    case 'payment_intent.succeeded':
      await handlePaymentSucceeded(event.data.object as Stripe.PaymentIntent);
      break;

    case 'charge.refunded':
      await handleRefund(event.data.object as Stripe.Charge);
      break;
  }

  return NextResponse.json({ received: true });
}

async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
  const purchaseId = session.metadata?.purchaseId;

  if (!purchaseId) {
    console.error('No purchase ID in session metadata');
    return;
  }

  // Update purchase status
  await db.purchase.update({
    where: { id: purchaseId },
    data: {
      status: 'COMPLETED',
      stripePaymentId: session.payment_intent as string
    }
  });

  // Fulfill the purchase
  const result = await fulfillPurchase(purchaseId);

  if (!result.success) {
    console.error('Fulfillment failed:', result.error);
    // Alert for manual intervention
    await alertFulfillmentFailure(purchaseId, result.error);
  }
}

async function handlePaymentSucceeded(paymentIntent: Stripe.PaymentIntent) {
  // Handle one-time payments
  const purchase = await db.purchase.findFirst({
    where: { stripePaymentId: paymentIntent.id }
  });

  if (purchase && purchase.status !== 'COMPLETED') {
    await db.purchase.update({
      where: { id: purchase.id },
      data: { status: 'COMPLETED' }
    });

    await fulfillPurchase(purchase.id);
  }
}

async function handleRefund(charge: Stripe.Charge) {
  const purchase = await db.purchase.findFirst({
    where: { stripePaymentId: charge.payment_intent as string }
  });

  if (purchase) {
    await db.purchase.update({
      where: { id: purchase.id },
      data: { status: 'REFUNDED' }
    });

    // Revoke licenses
    await db.license.updateMany({
      where: { purchaseId: purchase.id },
      data: { status: 'REVOKED' }
    });

    // Revoke access
    await db.userAccess.deleteMany({
      where: {
        userId: purchase.userId,
        productId: purchase.productId
      }
    });
  }
}

async function alertFulfillmentFailure(purchaseId: string, error?: string) {
  // Send alert to admin
  await sendEmail({
    to: process.env.ADMIN_EMAIL!,
    template: 'fulfillment-failure',
    data: { purchaseId, error }
  });
}
```

---

## Customer Download Page

```typescript
// app/purchases/[id]/page.tsx
import { db } from '@/lib/db';
import { auth } from '@/lib/auth';
import { createDownloadToken } from '@/lib/delivery/download-tokens';
import { redirect, notFound } from 'next/navigation';

interface Props {
  params: { id: string };
}

export default async function PurchasePage({ params }: Props) {
  const session = await auth();
  if (!session?.user) redirect('/login');

  const purchase = await db.purchase.findUnique({
    where: {
      id: params.id,
      userId: session.user.id
    },
    include: {
      product: true,
      licenses: true,
      downloads: {
        orderBy: { createdAt: 'desc' },
        take: 5
      }
    }
  });

  if (!purchase) notFound();

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">{purchase.product.name}</h1>

      <div className="bg-white rounded-lg shadow p-6 space-y-6">
        {/* Purchase Info */}
        <div className="border-b pb-4">
          <p className="text-sm text-gray-500">Purchase Date</p>
          <p>{new Date(purchase.createdAt).toLocaleDateString()}</p>
        </div>

        {/* Download Section */}
        {purchase.product.type === 'DOWNLOAD' && (
          <DownloadSection purchase={purchase} />
        )}

        {/* License Section */}
        {purchase.product.type === 'LICENSE' && (
          <LicenseSection licenses={purchase.licenses} />
        )}

        {/* Access Section */}
        {['SUBSCRIPTION', 'MEMBERSHIP', 'COURSE'].includes(purchase.product.type) && (
          <AccessSection product={purchase.product} />
        )}
      </div>
    </div>
  );
}

function DownloadSection({ purchase }: { purchase: any }) {
  const remainingDownloads = purchase.maxDownloads - purchase.downloadCount;

  return (
    <div>
      <h2 className="font-semibold mb-2">Download</h2>
      <p className="text-sm text-gray-600 mb-4">
        {remainingDownloads} downloads remaining
      </p>

      {remainingDownloads > 0 ? (
        <form action={generateNewDownload}>
          <input type="hidden" name="purchaseId" value={purchase.id} />
          <button
            type="submit"
            className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
          >
            Generate Download Link
          </button>
        </form>
      ) : (
        <p className="text-red-600">Download limit reached. Contact support.</p>
      )}

      {/* Recent Downloads */}
      {purchase.downloads.length > 0 && (
        <div className="mt-4">
          <h3 className="text-sm font-medium mb-2">Recent Downloads</h3>
          <ul className="text-sm text-gray-600 space-y-1">
            {purchase.downloads.map((d: any) => (
              <li key={d.id}>
                {new Date(d.createdAt).toLocaleString()} -
                {d.downloadedAt ? ' Downloaded' : ' Pending'}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
}

function LicenseSection({ licenses }: { licenses: any[] }) {
  return (
    <div>
      <h2 className="font-semibold mb-2">License Keys</h2>
      {licenses.map(license => (
        <div key={license.id} className="bg-gray-50 p-4 rounded mb-2">
          <code className="text-lg font-mono">{license.key}</code>
          <div className="text-sm text-gray-600 mt-2">
            <p>Status: {license.status}</p>
            <p>Activations: {license.activations} / {license.maxActivations}</p>
            {license.expiresAt && (
              <p>Expires: {new Date(license.expiresAt).toLocaleDateString()}</p>
            )}
          </div>
        </div>
      ))}
    </div>
  );
}

function AccessSection({ product }: { product: any }) {
  const accessUrl = getAccessUrl(product);

  return (
    <div>
      <h2 className="font-semibold mb-2">Access</h2>
      <a
        href={accessUrl}
        className="inline-block bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700"
      >
        Access {product.name}
      </a>
    </div>
  );
}

async function generateNewDownload(formData: FormData) {
  'use server';

  const purchaseId = formData.get('purchaseId') as string;
  const result = await createDownloadToken(purchaseId);

  redirect(result.downloadUrl);
}

function getAccessUrl(product: any): string {
  switch (product.type) {
    case 'COURSE':
      return `/courses/${product.metadata?.courseSlug}`;
    case 'MEMBERSHIP':
      return `/members`;
    default:
      return `/dashboard`;
  }
}
```

---

## Best Practices

| Aspect | Recommendation |
|--------|----------------|
| **URLs** | Signed, time-limited, single-use |
| **Limits** | 3-5 downloads per purchase |
| **Licenses** | Checksum validation, activation limits |
| **Email** | Immediate delivery confirmation |
| **Refunds** | Auto-revoke access and licenses |
| **Monitoring** | Track failed deliveries, alert admins |

---

## Fazit

Digital Product Delivery erfordert:

1. **Security**: Signierte URLs, Token, validierte Keys
2. **Automation**: Webhook-triggered Fulfillment
3. **Limits**: Download-Counts, Activation-Limits
4. **UX**: Klare Download-Pages, License-Management

Sichere Delivery schützt digitale Produkte und sorgt für gute Customer Experience.

---

## Bildprompts

1. "Digital product delivery flow diagram, download and license paths"
2. "License key activation interface, software registration screen"
3. "E-commerce download page, purchase confirmation with download button"

---

## Quellen

- [AWS S3 Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html)
- [Stripe Fulfillment](https://stripe.com/docs/payments/checkout/fulfill-orders)
- [Software Licensing Best Practices](https://www.keygen.sh/docs/guides)
- [Gumroad Digital Products](https://gumroad.com/features/digital-products)
