Operations & platform
Downloadable products with entitlement
Generated from docs/plan/core/downloadable-products-with-entitlement/PLAN.md in the AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
Downloadable product schema exists but delivery does not. This feature completes the downloadable product flow: signed download links, access tracking, expiry/limit management.
The problem
I sell digital files (eBooks, software, music) but I have no system to limit downloads, track who accessed what, or expire access after a period.
What it does
- Downloadable product entity: downloadable=true, downloads array [{filename, url, size_bytes}]
- Entitlement: customer can download after purchase; create an Entitlement record {order_id, product_id, customer_id, download_count, download_limit, expires_at}
- Signed download links: generate time-limited (24h), HMAC-signed URLs, e.g., /api/downloads/abc123?token=signed_jwt
- Download tracking: log each download with customer_id, timestamp, IP, filename; surface in admin order view
- Expiry: entitlement with expires_at in the past returns 403; customer can re-purchase to regain access
- Download limits: if limit=5, customer can download each file up to 5 times; 6th attempt fails
- API endpoints: create entitlement (on order creation), generate signed download link, get download history
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Streaming (video/audio without download) — files are downloaded/stored locally by customer
- DRM (digital rights management) or encryption — we provide no copy protection; rely on customer trust
- Peer-to-peer distribution or torrent — centralized download from our server only
Data model
Entitlement {id, order_id, product_id, customer_id, download_count, download_limit (nullable), expires_at, created_at}. DownloadLog {id, entitlement_id, filename, downloaded_at, ip_address}. Update Product schema to include downloads array.
API
- POST /api/downloads/create-entitlement — create entitlement (internal, called by order service on purchase)
- POST /api/downloads/:entitlement_id/link — generate signed download URL (returns URL)
- GET /api/downloads/:signed_token — perform download (verifies signature, checks expiry/limit, logs download)
- GET /api/orders/:order_id/downloads — download history for order (customer or admin)
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
Order detail, Downloads section: list of entitlements for this order with status (active/expired), download limit/count, expires_at, download history (table with filename, download count, last downloaded at).
The seam — why this is core
Core owns the entitlement model, signed URL generation, and REST API. This is table-stakes: if downloadable schema exists in the database, the download feature must exist.
If you ship the schema, ship the feature. Incomplete downloadables are worse than shipping nothing.
Dependencies
- Order system (already shipped)
- File storage (already shipping in core)
- JWT/HMAC signing library (assume available)
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- A customer purchases a downloadable product; an Entitlement is created with download_limit=unlimited, expires_at = 90 days from now
- Customer receives a download link; clicking it returns a file (HTTP 200 with Content-Disposition: attachment)
- Download is logged with customer_id, timestamp, and filename
- Customer who downloads 5 times when limit=5 can still download; 6th attempt fails (403 Forbidden)
- Customer after expiry date tries to download; request fails (403 Expired); customer can re-purchase
- Signed URL includes a timestamp; URL is only valid for 24 hours (rejects old URLs)
Risks
Download links must be signed and time-limited; unsigned URLs expose files to unauthorized access. Download count must be atomic; concurrent downloads from same customer could race and bypass limit — mutex required. If file is large (video), download must stream (not load entire file in memory). File deletion must be managed; if a file is deleted from storage but is still in an Entitlement, download fails (document as known limitation).
Commercial context
| Suggested price | Core |
| Rival anchor | Magento Open Source, free: full downloadable product support. |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and ../../AI-GUIDE.md for how to work on this repository.