Selling Ebooks: Protection, Delivery & Library Management
We've seen it many times: a client uploads a PDF to a public folder, the link spreads, and sales plummet. Piracy kills margins. Our experience shows that secure ebook sales require a well-designed storage architecture, temporary link generation, and optional watermarking. One of our projects — a technical literature store on Laravel 11 — saw piracy complaints drop by 90% after implementing signed URLs, and average order value increased due to buyer trust. We deliver such a solution turnkey in 3–4 days. Below are the technical details that help avoid common pitfalls and ensure digital goods security.
How to Protect Files from Unauthorized Copying?
The golden rule: files must never reside in a publicly accessible directory. No /public/books/my-book.pdf. Use S3-compatible object storage (AWS S3, Cloudflare R2, MinIO) with no public ACL. S3 is 10x faster and more reliable than a local disk — data is replicated and never lost.
After payment confirmation, the user receives a temporary signed URL:
// Laravel + AWS S3 $url = Storage::disk('s3')->temporaryUrl( "books/{$book->file_key}", now()->addHours(48), ['ResponseContentDisposition' => 'attachment; filename="' . $book->filename . '"'] ); The link expires in 48 hours. Users can re-download via their personal account — a new link is generated each time. We can limit download count: table download_attempts(purchase_id, downloaded_at), e.g., 5 downloads per purchase. This ensures that even if a link leaks, an attacker cannot exceed the limit.
More on signed URLs
Signed URLs are generated using S3 access keys and include an `Expires` parameter. The AWS SDK automatically signs the request. For extra security, use `ResponseContentDisposition` to force download rather than open in browser.What's Included in a Typical Integration?
We integrate Stripe with correct tax handling for digital goods. A key point: the taxability flag — digital books may be subject to VAT differently than physical goods in some jurisdictions. Stripe Tax can automatically determine this based on the payer's IP/address.
const session = await stripe.checkout.sessions.create({ mode: 'payment', line_items: [{ price: book.stripe_price_id, // pre-created Price in Stripe Dashboard quantity: 1, }], automatic_tax: { enabled: true }, metadata: { book_id: book.id, user_id: user.id }, success_url: `${APP_URL}/library?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${APP_URL}/books/${book.slug}`, }); Webhook checkout.session.completed creates a purchases record and sends an email with a download link. Using signed links is standard practice for digital goods.
Data Structure for Purchases and Downloads
CREATE TABLE purchases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id BIGINT REFERENCES users(id), book_id BIGINT REFERENCES books(id), payment_id VARCHAR(255) UNIQUE, amount_cents INT, currency VARCHAR(3), status VARCHAR(20) DEFAULT 'completed', -- completed | refunded created_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE download_attempts ( id BIGSERIAL PRIMARY KEY, purchase_id UUID REFERENCES purchases(id), ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP DEFAULT NOW() ); Formats: PDF vs EPUB
Most buyers prefer PDF for desktop reading and EPUB for mobile readers (Kindle, Apple Books, Kobo). Selling both formats in a single purchase is good practice. Store files with different suffixes in one bucket:
books/ {uuid}-original.epub {uuid}-print.pdf {uuid}-cover.jpg Table book_files(book_id, format, file_key, file_size_bytes).
Optional Watermark
For high-value books, adding a personalized watermark with the buyer's email can deter sharing. This isn't DRM but provides a psychological barrier. According to Wikipedia, digital watermarking is a copyright protection method embedded into content. PDF watermark using pypdf (Python) or iTextSharp (.NET). In the PHP ecosystem — setasign/fpdi:
use setasign\Fpdi\Fpdi; $pdf = new Fpdi(); $pageCount = $pdf->setSourceFile($sourcePath); for ($i = 1; $i <= $pageCount; $i++) { $pdf->AddPage(); $pdf->useTemplate($pdf->importPage($i)); $pdf->SetFont('Helvetica', '', 8); $pdf->SetTextColor(180, 180, 180); $pdf->SetXY(10, 285); $pdf->Write(0, "Licensed to: {$purchase->user->email}"); } $pdf->Output($outputPath, 'F'); Generation happens asynchronously in a queue (Laravel Jobs / Bull / Celery), then the link is updated. For books under 10 MB, this takes 2–5 seconds. Automation saves you manual effort.
Email Delivery & Personal Library
After purchase, a download button email should arrive within 30–60 seconds. Don't do it synchronously in the HTTP request — send via queue:
// In webhook handler and Job ProcessPurchase::dispatch($purchase)->onQueue('purchases'); class ProcessPurchase implements ShouldQueue { public function handle() { // 1. Generate watermark (optional) // 2. Create signed URL // 3. Send email Mail::to($this->purchase->user)->send( new BookPurchasedMail($this->purchase, $downloadUrl) ); } } Users should be able to re-download via /library. That page lists all purchases with a "Download" button that triggers a new temporary URL generation. Storing a permanent link in the database is pointless; it will expire.
What's Included in the Work
| Stage | Details |
|---|---|
| Analysis | Determine protection requirements, select S3, payment system |
| Design | Database schema, delivery architecture, webhook setup |
| Implementation | Integrate Stripe, S3, link generation, watermark |
| Testing | Verify payment, download, refunds |
| Deployment | Deploy to production, set up monitoring |
| Documentation | API description, admin instructions |
| Support | 30 days post-deploy: consultations, bug fixes |
We also train your team on the admin panel and provide source code access. Contact us to evaluate your project.
Protection Method Comparison
| Method | Reliability | Speed | Complexity |
|---|---|---|---|
| Signed S3 URLs | High | Instant | Low |
| Watermark | Medium | 2–5 sec | Medium |
| DRM (Adobe, LCP) | Maximum | Depends on service | High |
Estimated Timelines
| Task | Time |
|---|---|
| File upload, S3, protection | 1 day |
| Payment integration + webhook | 1–2 days |
| Personal library, re-download | 1 day |
| Email delivery | 0.5 day |
| PDF watermark | 1–2 days |
Basic implementation without watermark: 3–4 days. Pricing is individual. Get a consultation — we'll find the optimal solution for your budget.
How We Work: Step by Step
- Analysis — We study your current stack and protection requirements.
- Design — We create a database schema and delivery architecture.
- Integration — We connect Stripe, S3, generate signed URLs.
- Testing — We verify payment, download, and refund scenarios.
- Deployment — We go live and set up monitoring.
Each stage ends with a demo to the client. We guarantee transparency and deadlines. This approach cuts time-to-market from two weeks to three days, saving up to 70% of development budget.







