digital-products

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Digital Products

数字产品

Overview

概述

Selling digital products — ebooks, software licenses, templates, music, courses — requires secure delivery after payment, download limits to prevent unauthorized sharing, and expiring access links. Every major platform either has this built in (WooCommerce) or has a mature app that handles it (Shopify, BigCommerce). Use the platform's native solution first; only build custom delivery infrastructure if your use case (complex license key management, subscription-gated content libraries) exceeds what apps offer.
销售数字产品——电子书、软件许可证、模板、音乐、课程——需要在付款后进行安全交付,设置下载限制以防止未经授权的共享,并提供过期的访问链接。各大主流平台要么内置了该功能(如WooCommerce),要么有成熟的应用来处理(如Shopify、BigCommerce)。优先使用平台的原生解决方案;只有当你的使用场景(复杂的许可证密钥管理、订阅 gated 内容库)超出应用提供的功能时,才需要构建自定义交付基础设施。

When to Use This Skill

何时使用该技能

  • When adding downloadable products (PDFs, software, audio, templates) to an existing store
  • When implementing a license key delivery system for software products
  • When building a subscription that gates access to a digital content library
  • When replacing publicly accessible download URLs with secure, expiring signed URLs
  • 为现有店铺添加可下载产品(PDF、软件、音频、模板)时
  • 为软件产品实现许可证密钥交付系统时
  • 构建订阅专属的数字内容库访问权限控制时
  • 将公开可访问的下载URL替换为安全、过期的签名URL时

Core Instructions

核心操作指南

Step 1: Determine platform and choose the right tool

步骤1:确定平台并选择合适工具

PlatformRecommended ToolWhy
ShopifySky Pilot or FileflareSky Pilot handles files, license keys, streaming video, and download limits; Fileflare is simpler for basic file downloads
WooCommerceWooCommerce Downloadable Products (built-in)WooCommerce has native digital product support including download limits, expiry, and secure token URLs
BigCommerceDownloadable Digital Products (built-in) or FileflareBigCommerce handles file attachment and download delivery natively
Custom / HeadlessBuild delivery with S3 presigned URLsRequired when the platform has no native digital product support

平台推荐工具原因
ShopifySky Pilot 或 FileflareSky Pilot 支持文件管理、许可证密钥、视频流及下载限制;Fileflare 更适用于基础文件下载场景
WooCommerceWooCommerce Downloadable Products(内置)WooCommerce 原生支持数字产品,包括下载限制、过期设置及安全令牌URL
BigCommerceDownloadable Digital Products(内置)或 FileflareBigCommerce 原生支持文件附件与下载交付
自定义/无头架构基于S3预签名URL构建交付系统当平台无原生数字产品支持时需使用该方案

Step 2: Platform-specific setup

步骤2:平台专属设置



Shopify

Shopify

Shopify does not have native digital product support — you need an app. Sky Pilot is the most feature-complete option.
Setting up Sky Pilot:
  1. Install Sky Pilot from the Shopify App Store
  2. Go to Sky Pilot → Files → Upload your digital file (PDF, ZIP, etc.)
  3. Go to Sky Pilot → Products and link the uploaded file to a specific product or variant
  4. Configure delivery settings:
    • Download limit: set to 3–5 for standard products, unlimited for subscriptions
    • Link expiry: 48–72 hours is standard; customers can request a new link from their account
    • Automatic delivery: enabled by default — customers receive a download email immediately after payment
  5. Customize the delivery email under Sky Pilot → Settings → Email template
For license key products:
  1. In Sky Pilot, go to License Keys → Import and upload a CSV of your license keys
  2. Link the license key pool to the product
  3. Sky Pilot automatically assigns one key per purchase and includes it in the delivery email
For subscription-gated content (Sky Pilot + Recharge):
  • Sky Pilot integrates with Recharge — customers with an active subscription automatically gain access; access is revoked when the subscription cancels

Shopify 无原生数字产品支持——你需要使用应用。Sky Pilot 是功能最全面的选项。
Sky Pilot 设置流程:
  1. 从 Shopify App Store 安装 Sky Pilot
  2. 进入 Sky Pilot → Files → Upload 上传你的数字文件(PDF、ZIP等)
  3. 进入 Sky Pilot → Products,将上传的文件关联至特定产品或变体
  4. 配置交付设置:
    • 下载限制:标准产品设置为3–5次,订阅产品设为无限制
    • 链接过期时间:标准为48–72小时;客户可从账户中申请新链接
    • 自动交付:默认启用——客户付款后立即收到下载邮件
  5. Sky Pilot → Settings → Email template 下自定义交付邮件
针对许可证密钥产品:
  1. 在 Sky Pilot 中,进入 License Keys → Import 上传许可证密钥的CSV文件
  2. 将许可证密钥池关联至产品
  3. Sky Pilot 会自动为每笔订单分配一个密钥,并将其包含在交付邮件中
针对订阅 gated 内容(Sky Pilot + Recharge):
  • Sky Pilot 与 Recharge 集成——订阅处于活跃状态的客户自动获得访问权限;订阅取消后访问权限将被收回

WooCommerce

WooCommerce

WooCommerce has digital product delivery built in — no extra plugin required for basic use.
Setting up a downloadable product:
  1. Go to WooCommerce → Products → Add Product
  2. Check Downloadable (and optionally Virtual to skip shipping)
  3. Under Product Data → Downloadable Files:
    • Click Add File and upload the file or enter a URL
    • File name: the name shown to customers in their account
  4. Configure access settings:
    • Download limit: enter a number (e.g.,
      5
      ) or leave blank for unlimited
    • Download expiry: number of days after purchase (e.g.,
      365
      ), or leave blank for no expiry
  5. WooCommerce generates a secure, token-based download URL for each purchase automatically
Forcing customer account for downloads:
  • Go to WooCommerce → Settings → Accounts & Privacy
  • Enable Grant access to downloadable products after payment and Require login to download
For license key products:
  • Install License Manager for WooCommerce (free plugin)
  • Go to License Manager → Add License Keys and bulk import your keys
  • Link the license key pool to a product
  • The plugin delivers keys in the order confirmation email and the customer's account page

WooCommerce 内置数字产品交付功能——基础使用无需额外插件。
设置可下载产品:
  1. 进入 WooCommerce → Products → Add Product
  2. 勾选 Downloadable(可选择勾选 Virtual 以跳过物流设置)
  3. Product Data → Downloadable Files 下:
    • 点击 Add File 上传文件或输入URL
    • 文件名:显示给客户的文件名称(在其账户中)
  4. 配置访问设置:
    • 下载限制:输入数字(如
      5
      ),留空则表示无限制
    • 下载过期时间:购买后的天数(如
      365
      ),留空则表示永不过期
  5. WooCommerce 会自动为每笔订单生成安全的基于令牌的下载URL
强制客户登录以下载:
  • 进入 WooCommerce → Settings → Accounts & Privacy
  • 启用 付款后授予可下载产品访问权限需登录才能下载
针对许可证密钥产品:
  • 安装 License Manager for WooCommerce(免费插件)
  • 进入 License Manager → Add License Keys 批量导入密钥
  • 将许可证密钥池关联至产品
  • 插件会在订单确认邮件和客户账户页面中交付密钥

BigCommerce

BigCommerce

Setting up digital delivery (built-in):
  1. Go to Products → Add Product
  2. Under Files, upload your digital file (BigCommerce supports files up to 512 MB)
  3. Set Max downloads to limit how many times the file can be downloaded per order
  4. BigCommerce sends an automatic download email after payment with a secure link
For more advanced delivery:
  • Install SendOwl or Fileflare from the BigCommerce App Marketplace
  • These apps add license key management, download analytics, and streaming video delivery

设置数字交付(内置功能):
  1. 进入 Products → Add Product
  2. Files 下上传数字文件(BigCommerce 支持最大512 MB的文件)
  3. 设置 Max downloads 以限制每笔订单的文件下载次数
  4. BigCommerce 会在付款后自动发送包含安全链接的下载邮件
如需更高级的交付功能:
  • 从 BigCommerce App Marketplace 安装 SendOwlFileflare
  • 这些应用可添加许可证密钥管理、下载分析及视频流交付功能

Custom / Headless

自定义/无头架构

For headless storefronts, implement secure file delivery using S3 presigned URLs — never expose the raw S3 key:
typescript
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: process.env.AWS_REGION });

// Generate a short-lived presigned URL for each download attempt
export async function generateDownloadUrl(orderId: string, digitalProductId: string) {
  const access = await db.orderDigitalAccess.findUnique({
    where: { orderId_digitalProductId: { orderId, digitalProductId } },
    include: { digitalProduct: true },
  });

  if (!access) throw new Error('No access record found');
  if (access.expiresAt && access.expiresAt < new Date()) throw new Error('Download access has expired');
  if (access.maxDownloads && access.downloadCount >= access.maxDownloads) throw new Error('Download limit reached');

  // Atomically increment download count
  await db.orderDigitalAccess.update({ where: { id: access.id }, data: { downloadCount: { increment: 1 } } });

  // 60-second presigned URL — short enough to prevent sharing, long enough for the redirect
  const command = new GetObjectCommand({
    Bucket: process.env.DIGITAL_PRODUCTS_BUCKET,
    Key: access.digitalProduct.fileStorageKey,
    ResponseContentDisposition: `attachment; filename="${access.digitalProduct.fileName}"`,
  });

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

// Provision access after payment — call this from your payment webhook
export async function provisionDigitalAccess(orderId: string) {
  const order = await db.orders.findUnique({
    where: { id: orderId },
    include: { items: { include: { variant: { include: { digitalProduct: true } } } } },
  });

  for (const item of order.items.filter(i => i.variant.digitalProduct)) {
    const dp = item.variant.digitalProduct;
    await db.orderDigitalAccess.upsert({
      where: { orderId_digitalProductId: { orderId, digitalProductId: dp.id } },
      create: {
        orderId, digitalProductId: dp.id, downloadCount: 0,
        maxDownloads: dp.downloadLimit,
        expiresAt: dp.accessDurationDays ? new Date(Date.now() + dp.accessDurationDays * 86400000) : null,
      },
      update: {}, // Idempotent — webhooks can fire multiple times
    });
  }
}

// License key pool management
export async function assignLicenseKey(orderId: string, productId: string) {
  return db.$transaction(async tx => {
    const license = await tx.digitalProductLicenses.findFirst({
      where: { productId, status: 'available' },
    });
    if (!license) throw new Error(`No available license keys for product ${productId}`);
    await tx.digitalProductLicenses.update({
      where: { id: license.id },
      data: { status: 'sold', orderId, soldAt: new Date() },
    });
    return license.licenseKey;
  });
}

对于无头店铺,使用S3预签名URL实现安全文件交付——切勿暴露原始S3密钥:
typescript
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({ region: process.env.AWS_REGION });

// Generate a short-lived presigned URL for each download attempt
export async function generateDownloadUrl(orderId: string, digitalProductId: string) {
  const access = await db.orderDigitalAccess.findUnique({
    where: { orderId_digitalProductId: { orderId, digitalProductId } },
    include: { digitalProduct: true },
  });

  if (!access) throw new Error('No access record found');
  if (access.expiresAt && access.expiresAt < new Date()) throw new Error('Download access has expired');
  if (access.maxDownloads && access.downloadCount >= access.maxDownloads) throw new Error('Download limit reached');

  // Atomically increment download count
  await db.orderDigitalAccess.update({ where: { id: access.id }, data: { downloadCount: { increment: 1 } } });

  // 60-second presigned URL — short enough to prevent sharing, long enough for the redirect
  const command = new GetObjectCommand({
    Bucket: process.env.DIGITAL_PRODUCTS_BUCKET,
    Key: access.digitalProduct.fileStorageKey,
    ResponseContentDisposition: `attachment; filename="${access.digitalProduct.fileName}"`,
  });

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

// Provision access after payment — call this from your payment webhook
export async function provisionDigitalAccess(orderId: string) {
  const order = await db.orders.findUnique({
    where: { id: orderId },
    include: { items: { include: { variant: { include: { digitalProduct: true } } } } },
  });

  for (const item of order.items.filter(i => i.variant.digitalProduct)) {
    const dp = item.variant.digitalProduct;
    await db.orderDigitalAccess.upsert({
      where: { orderId_digitalProductId: { orderId, digitalProductId: dp.id } },
      create: {
        orderId, digitalProductId: dp.id, downloadCount: 0,
        maxDownloads: dp.downloadLimit,
        expiresAt: dp.accessDurationDays ? new Date(Date.now() + dp.accessDurationDays * 86400000) : null,
      },
      update: {}, // Idempotent — webhooks can fire multiple times
    });
  }
}

// License key pool management
export async function assignLicenseKey(orderId: string, productId: string) {
  return db.$transaction(async tx => {
    const license = await tx.digitalProductLicenses.findFirst({
      where: { productId, status: 'available' },
    });
    if (!license) throw new Error(`No available license keys for product ${productId}`);
    await tx.digitalProductLicenses.update({
      where: { id: license.id },
      data: { status: 'sold', orderId, soldAt: new Date() },
    });
    return license.licenseKey;
  });
}

Step 3: Configure post-purchase delivery email

步骤3:配置购买后交付邮件

All platforms send an automatic delivery email — customize it to be clear and professional:
Content to include:
  • Order confirmation number
  • Product name and description
  • Download link (or license key, displayed prominently)
  • Download limit and expiry date if applicable
  • Instructions for how to access/install the product
  • Support contact if they have trouble
Shopify (Sky Pilot): Customize under Sky Pilot → Settings → Email Template
WooCommerce: Customize under WooCommerce → Settings → Emails → Customer Processing Order (includes download links automatically)
BigCommerce: Customize under Marketing → Transactional Emails → Order Status Notification

所有平台都会发送自动交付邮件——需将其自定义为清晰专业的样式:
需包含的内容:
  • 订单确认编号
  • 产品名称与描述
  • 下载链接(或许可证密钥,需突出显示)
  • 下载限制与过期日期(如有)
  • 产品访问/安装说明
  • 问题反馈的支持联系方式
Shopify(Sky Pilot):Sky Pilot → Settings → Email Template 下自定义
WooCommerce:WooCommerce → Settings → Emails → Customer Processing Order 下自定义(自动包含下载链接)
BigCommerce:Marketing → Transactional Emails → Order Status Notification 下自定义

Step 4: Monitor license key inventory

步骤4:监控许可证密钥库存

For license key products, set up alerts before keys run out:
  • Sky Pilot: Go to License Keys → [Product] — shows remaining key count; Sky Pilot sends email alerts when stock drops below a threshold you configure
  • License Manager for WooCommerce: Dashboard shows keys remaining per product with color-coded warnings
  • Custom: Run a daily check and alert when available keys drop below 10
对于许可证密钥产品,在密钥耗尽前设置警报:
  • Sky Pilot: 进入 License Keys → [Product]——显示剩余密钥数量;当库存低于你配置的阈值时,Sky Pilot 会发送邮件警报
  • License Manager for WooCommerce: 仪表盘显示每个产品的剩余密钥数量,并带有颜色编码的警告
  • 自定义方案: 每日运行检查,当可用密钥数量低于10时触发警报

Best Practices

最佳实践

  • Never deliver digital products until payment is confirmed — trigger delivery from the payment confirmed webhook, not the order created event; card declines happen after order creation
  • Use short-lived download links (60 seconds) for headless implementations — generate the URL fresh on each download page load, not when the page renders
  • Set download limits for most products — 3–5 downloads is standard; unlimited for subscription access; limits prevent casual file sharing
  • Store files in private buckets — never make your S3/GCS bucket public; all access must go through signed URL generation
  • Monitor license key inventory — running out of keys causes support tickets and chargebacks; set up alerts when below 20% of original inventory
  • Send a separate, dedicated delivery email — don't bundle license keys into the generic order confirmation; a focused delivery email is easier for customers to find and reference
  • 付款确认前切勿交付数字产品——从付款确认的webhook触发交付,而非订单创建事件;订单创建后可能会出现信用卡拒付情况
  • 无头架构使用短时效下载链接(60秒)——在每次下载页面加载时生成新的URL,而非页面渲染时生成
  • 为大多数产品设置下载限制——标准为3–5次下载;订阅访问设为无限制;限制可防止随意文件共享
  • 将文件存储在私有存储桶中——切勿将S3/GCS存储桶设为公开;所有访问必须通过签名URL生成
  • 监控许可证密钥库存——密钥耗尽会导致支持工单和退款;当库存低于原始库存的20%时设置警报
  • 发送单独的专属交付邮件——不要将许可证密钥捆绑到通用订单确认邮件中;聚焦的交付邮件更便于客户查找和参考

Common Pitfalls

常见陷阱

ProblemSolution
Download link expires before customer clicks itFor headless builds, generate a fresh presigned URL on each page load, not at email send time
Digital product delivered after a failed paymentTrigger delivery only from the payment success webhook (
payment_intent.succeeded
in Stripe), never from order creation
License keys double-assignedUse a database transaction for key assignment with an atomic status check — never select and then update in two steps
Customer loses access after account deletionStore download access against
order_id
+
email
, not only the user account ID; allow access recovery via order number
Large file download times outUse S3 presigned URLs to deliver files directly from S3 to the customer's browser — never proxy through your server
问题解决方案
下载链接在客户点击前已过期对于无头架构,在每次页面加载时生成新的预签名URL,而非发送邮件时生成
付款失败后仍交付数字产品仅从付款成功的webhook触发交付(如Stripe中的
payment_intent.succeeded
),切勿从订单创建事件触发
许可证密钥被重复分配使用数据库事务进行密钥分配,并进行原子状态检查——切勿分两步执行查询和更新操作
客户删除账户后失去访问权限将下载权限与
order_id
+
email
关联,而非仅关联用户账户ID;允许通过订单编号恢复访问权限
大文件下载超时使用S3预签名URL直接从S3向客户浏览器交付文件——切勿通过你的服务器代理

Related Skills

相关技能

  • @inventory-tracking
  • @low-stock-alerts
  • @product-data-modeling
  • @inventory-tracking
  • @low-stock-alerts
  • @product-data-modeling