skills/catalog-inventory/digital-products/SKILL.md
Sell software, ebooks, and other downloads with secure delivery, license key generation, download limits, and expiration controls using platform apps
npx skillsauth add finsilabs/awesome-ecommerce-skills digital-productsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
| Platform | Recommended Tool | Why | |----------|-----------------|-----| | Shopify | Sky Pilot or Fileflare | Sky Pilot handles files, license keys, streaming video, and download limits; Fileflare is simpler for basic file downloads | | WooCommerce | WooCommerce Downloadable Products (built-in) | WooCommerce has native digital product support including download limits, expiry, and secure token URLs | | BigCommerce | Downloadable Digital Products (built-in) or Fileflare | BigCommerce handles file attachment and download delivery natively | | Custom / Headless | Build delivery with S3 presigned URLs | Required when the platform has no native digital product support |
Shopify does not have native digital product support — you need an app. Sky Pilot is the most feature-complete option.
Setting up Sky Pilot:
For license key products:
For subscription-gated content (Sky Pilot + Recharge):
WooCommerce has digital product delivery built in — no extra plugin required for basic use.
Setting up a downloadable product:
5) or leave blank for unlimited365), or leave blank for no expiryForcing customer account for downloads:
For license key products:
Setting up digital delivery (built-in):
For more advanced delivery:
For headless storefronts, implement secure file delivery using S3 presigned URLs — never expose the raw S3 key:
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;
});
}
All platforms send an automatic delivery email — customize it to be clear and professional:
Content to include:
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
For license key products, set up alerts before keys run out:
| Problem | Solution |
|---------|----------|
| Download link expires before customer clicks it | For headless builds, generate a fresh presigned URL on each page load, not at email send time |
| Digital product delivered after a failed payment | Trigger delivery only from the payment success webhook (payment_intent.succeeded in Stripe), never from order creation |
| License keys double-assigned | Use a database transaction for key assignment with an atomic status check — never select and then update in two steps |
| Customer loses access after account deletion | Store download access against order_id + email, not only the user account ID; allow access recovery via order number |
| Large file download times out | Use S3 presigned URLs to deliver files directly from S3 to the customer's browser — never proxy through your server |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion