AstroBaaS

Operations & platform

Downloadable products with entitlement

Free — GPL coresize Midea, not a commitment

Generated from docs/plan/core/downloadable-products-with-entitlement/PLAN.md in the project's planning notes, which are not part of the public repository. Nothing described below is implemented — it is the written plan for it.

An idea, not a commitment. It has a written plan but no place in the build order and no date. What is committed.

Downloadable product schema exists but delivery does not. This feature completes the downloadable product flow: signed download links, access tracking, expiry/limit management.

Partly built (2026-10-26). The core now ships download entitlements: src/lib/commerce/download-entitlements.ts, tested in tests/download-entitlements.test.mjs. It covers a download limit per file, an expiry per entitlement, a signed link (valid for 24 hours by default, configurable from 1 hour to 30 days), an atomic download count, a log of time, file and IP, and an admin panel on the order. Different from this page: entitlements are granted when the order is paid, not when it is created, and are stored on the order rather than in new tables. There are no separate entitlement or link routes: staff use /api/orders/{id}/downloads and the buyer /api/downloads?receipt=. Not built: a file size on each download (size_bytes), and a full download history: the log keeps the newest 50 entries and no customer id. This item stays planned until the rest is built or the plan is narrowed.

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 priceCore
Rival anchorMagento 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 CONTRIBUTING.md for how to work on this repository.