# 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 { 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 { 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 { 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 { 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 { await db.userAccess.create({ data: { userId, productId, grantedAt: new Date() } }); } async function enrollInCourse(userId: string, productId: string): Promise { 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 { 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 (

{purchase.product.name}

{/* Purchase Info */}

Purchase Date

{new Date(purchase.createdAt).toLocaleDateString()}

{/* Download Section */} {purchase.product.type === 'DOWNLOAD' && ( )} {/* License Section */} {purchase.product.type === 'LICENSE' && ( )} {/* Access Section */} {['SUBSCRIPTION', 'MEMBERSHIP', 'COURSE'].includes(purchase.product.type) && ( )}
); } function DownloadSection({ purchase }: { purchase: any }) { const remainingDownloads = purchase.maxDownloads - purchase.downloadCount; return (

Download

{remainingDownloads} downloads remaining

{remainingDownloads > 0 ? (
) : (

Download limit reached. Contact support.

)} {/* Recent Downloads */} {purchase.downloads.length > 0 && (

Recent Downloads

    {purchase.downloads.map((d: any) => (
  • {new Date(d.createdAt).toLocaleString()} - {d.downloadedAt ? ' Downloaded' : ' Pending'}
  • ))}
)}
); } function LicenseSection({ licenses }: { licenses: any[] }) { return (

License Keys

{licenses.map(license => (
{license.key}

Status: {license.status}

Activations: {license.activations} / {license.maxActivations}

{license.expiresAt && (

Expires: {new Date(license.expiresAt).toLocaleDateString()}

)}
))}
); } function AccessSection({ product }: { product: any }) { const accessUrl = getAccessUrl(product); return ( ); } 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)