Guides · March 12, 2023
Digital Downloads & License Delivery in a Headless Store
Selling digital downloads in a headless Next.js store means treating fulfillment as a webhook-driven, security-conscious workflow, not a checkbox in a page builder. Here is how to architect signed-URL delivery, license-key issuance, and an account library that actually holds up.
By Polo Themes
Selling a digital download is not the same problem as selling a physical product with a different shipping label. In a headless Next.js store, digital fulfillment has to be its own workflow: a payment webhook triggers file access, a signed URL or license key gets generated and bound to the order, and the buyer needs a durable way to come back for it later. Get any one of those steps wrong and you either lock out a paying customer or hand a permanent public link to your product file. This tutorial walks through the architecture end to end — storage, delivery, licensing, security, and UX — using patterns that hold up whether you are on Medusa, Stripe-only checkout, or a Shopify backend behind a headless frontend.
Why Digital Delivery Breaks Default Commerce Assumptions
Most commerce platforms — Shopify included — are modeled around physical fulfillment: an order is placed, an address is validated, a shipment is created, and the order eventually moves to "fulfilled." Digital products do not fit that model cleanly. There is no address, no carrier, and "fulfilled" should mean "the buyer now has working access," which can happen in milliseconds or need to persist for years. If you bolt a digital product onto a physical-first commerce backend without rethinking the flow, you get common failure modes: download links attached directly to a static file path (so the URL itself becomes the product, shareable forever), no record of who is entitled to what after the order confirmation email is lost, and no way to revoke access after a refund or chargeback.
In a headless setup you control the full path from checkout to delivery, which is an advantage — you are not fighting a theme's built-in "digital product" app — but it also means you own every edge case a specialized platform would otherwise have solved for you. The rest of this guide treats that ownership as the point: a properly built digital-delivery pipeline is a real feature, not a shortcut.
The Three Delivery Models, and When Each One Fits
Before writing code, decide which delivery model actually matches the product. Conflating them is the most common design mistake in digital commerce builds.
- Direct file download — a signed, time-limited URL to a static asset (a PDF, a Figma export, a theme .zip). Best for one-time creative or informational goods where there is nothing to "activate."
- License key — a generated code bound to the order and often to a device or domain, checked by the product itself at runtime (a plugin, a font, a self-hosted app). Best when the product runs on the customer's infrastructure and you need to gate functionality, seats, or renewal.
- Account-gated access — no file changes hands at all; purchase unlocks a resource behind the buyer's login (a private repo invite, a hosted dashboard, a component registry entry). Best for products that update after purchase and where you want a single source of truth rather than a file the customer may go stale on.
A lot of real digital products are actually a combination — a design asset bundle might be a direct download plus an account library entry so the buyer can re-download after clearing their disk. Build the direct-download and license-key primitives first; account gating is mostly a UI layer on top of the entitlement record you already need.
The Core Architecture
Regardless of which commerce backend sits behind your Next.js frontend, the pipeline has the same five stages, and each one should be a separate, testable unit rather than logic jammed into a single API route.
1. Payment confirmation via webhook, never client-side
Never grant access because the client redirected to a "success" page. Redirects can be replayed, spoofed, or simply never happen if the browser tab closes. The only trustworthy signal is a server-to-server webhook — Stripe's checkout.session.completed, Medusa's order.placed event, or a Shopify orders/paid webhook — verified with the provider's signing secret. Treat this webhook handler as the single entry point that creates an entitlement record; everything downstream reads from that record, not from the payment event again.
2. An entitlement table, not a file path in an email template
Create a row per (customer, product, order) with fields for status (active, refunded, revoked), download count or license seats used, and an expiry if applicable. This table is the actual product of your digital-delivery system — the file storage and the license generator are just things it points to. Without it you have no way to answer "does this person still have access" six months later, no way to support a "resend my download" request without re-triggering a purchase, and no way to revoke access cleanly on a refund webhook.
3. Signed, short-lived URLs for the actual file
Store the deliverable in private object storage (S3, Cloudflare R2, or equivalent) with public access disabled entirely. Generate a presigned URL server-side, scoped to a short expiry window — minutes for a one-time download link sent in an email, longer if it is generated on demand from an authenticated account page. The Next.js route that issues the URL should check the entitlement table first, log the issuance, and only then call the storage SDK's presign function. This is the difference between "the file is protected by a login" and "the file is protected by nothing except that the URL is long" — the latter is not protection, it is obscurity, and obscured URLs end up on forums and Discord servers within days of a popular launch.
4. License-key generation and binding
For products that need runtime activation, generate the key server-side at the same point the entitlement row is created — never let the client generate or supply a key. A reasonable default is a signed token (HMAC over the order ID, product ID, and issue date, or a JWT if you need embedded claims) rather than a random string with no relationship to your data; a signed key means your activation endpoint can validate structurally before it even hits the database, which cuts down on brute-force license-guessing traffic. Bind the key to the entitlement row so seat limits, revocation, and renewal all read from the same source of truth as the file-download path.
5. An activation endpoint with real limits
Whatever calls home to validate the license — a CLI, a plugin, a desktop app — needs an endpoint that checks the key's signature, looks up the entitlement, and enforces whatever binding model you chose (device fingerprint, domain, or a simple activation counter with a self-service "deactivate a device" flow). Rate-limit this endpoint aggressively; it is the part of the system most likely to get hit by scripted validation attempts against leaked keys.
Backend-Specific Notes
The five-stage pipeline above is backend-agnostic, but the amount of scaffolding your commerce layer gives you for free varies a lot.
Medusa
Medusa's fulfillment-provider abstraction is a genuinely good fit for digital goods: you can register a custom fulfillment provider whose "fulfill" step does nothing more than write your entitlement row and kick off the signed-URL or license flow, then mark the order fulfilled through the normal order-status machinery instead of inventing a parallel status field. Because Medusa emits a real event bus (order.placed, and your own custom events), the webhook-to-entitlement step in stage one above becomes an event subscriber rather than a bespoke webhook parser, which is less brittle across Medusa upgrades.
Stripe-only checkout (no full commerce backend)
If you are running Stripe Checkout directly against a Next.js app with no separate commerce layer, the entitlement table lives in your own database and the webhook handler is a Next.js route handler verifying Stripe's signature. This is the leanest version of the architecture and is genuinely sufficient for a large share of solo-creator digital products — resist the urge to add a full commerce platform if Stripe plus a Postgres table plus S3 covers the delivery models you actually need.
Shopify
Shopify's checkout and order model has no first-party concept of a digital download; every store selling digital goods on Shopify does so through a dedicated digital-delivery app (or custom app logic against the Admin API) layered on top. If your storefront is headless against Shopify, the practical approach is the same five-stage pipeline, driven off Shopify's orders/paid webhook, with your own entitlement table and storage rather than relying on theme-level "digital product" liquid snippets, which do not exist in a headless frontend anyway.
Security and Anti-Piracy, Realistically
No download-delivery system stops a determined buyer from redistributing a file once it is on their disk — accept that going in, and design for reducing casual sharing and catching abuse rather than making piracy impossible. Short-lived signed URLs stop link-forwarding, which is the overwhelmingly common casual case. Rate-limiting both the URL-issuance endpoint and any license-activation endpoint stops scripted abuse. Logging every issuance with an IP and timestamp gives you the evidence you need if a key shows up being resold. Lightweight watermarking — embedding the order ID or buyer email in a PDF footer or a font's internal metadata — deters casual redistribution without adding real friction for legitimate buyers, and it is far less invasive than DRM schemes that tend to punish paying customers more than pirates.
The Delivery UX: Confirmation Page, Email, and Account Library
Treat delivery as three redundant surfaces rather than one email that might land in spam. The order-confirmation page should render the download link or license key immediately after payment succeeds, read live from your entitlement API — not baked into a static success page. The confirmation email should carry the same link or key as a durable backup, generated at send time rather than reused from the confirmation-page render, since the page's URL may already be near its expiry by the time the email is opened. An account "library" page, gated behind login, should let a buyer re-issue a fresh signed URL or view an active license on demand, which eliminates the single largest source of support tickets in digital commerce: "I lost my download link."
Where This Fits for Design-Asset Sellers
Design-asset businesses live entirely inside this problem — a Figma file or a theme package is a pure direct-download product with no physical fulfillment fallback to lean on. Our own Figma UI kits and Shopify themes are sold the same way any headless digital-download store should think about delivery: an order should always map to a durable, re-issuable entitlement, not a one-time link that quietly stops working the moment a customer clears their downloads folder. If you are building a headless storefront for your own design or software product, the architecture above — entitlement table first, signed URLs and license keys as thin layers on top of it — will scale from a single-product Stripe integration up through a full multi-product Medusa catalog without a rewrite.
Frequently Asked Questions
Do I need a database, or can I get away with just Stripe and S3?
You need somewhere to persist the entitlement record — who bought what, and whether access is still active. That can be a lightweight Postgres table, not a full commerce platform, but skipping it entirely means you have no way to support refunds, resends, or license revocation later.
How long should a signed download URL last?
Minutes, not days, for links generated on demand from an authenticated session — the account page can always mint a fresh one. For links embedded in a confirmation email that might be opened days later, a longer window (an hour or so) balances security against the buyer actually being able to use the link they were sent.
Can I sell digital downloads through Shopify without a headless frontend?
Yes, through a dedicated digital-delivery app in the Shopify admin, since Shopify's core checkout has no native digital-fulfillment concept. The architecture in this guide becomes relevant once you go headless, at which point you are building that delivery logic yourself against Shopify's webhooks instead of relying on a theme app extension.
What is the single most common mistake in DIY digital delivery?
Treating the file's URL as the access control — a "secret" but permanent link with no expiry and no entitlement check behind it. It works fine until the link gets shared once, at which point there is no way to revoke it without breaking access for every legitimate buyer who has that same static link.