Digital Product Delivery
Digitale Produkte sicher ausliefern. Download-Links, License Keys, E-Books und Software mit Node.js und Next.js automatisiert bereitstellen.

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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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
// 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:
- Security: Signierte URLs, Token, validierte Keys
- Automation: Webhook-triggered Fulfillment
- Limits: Download-Counts, Activation-Limits
- UX: Klare Download-Pages, License-Management
Sichere Delivery schützt digitale Produkte und sorgt für gute Customer Experience.
Bildprompts
- "Digital product delivery flow diagram, download and license paths"
- "License key activation interface, software registration screen"
- "E-commerce download page, purchase confirmation with download button"