A decentralized pay-per-document marketplace built with Next.js, Supabase, and Bitcoin SV (BSV) blockchain. Upload documents, set prices, and get paid in cryptocurrency.
Live Demo: https://ppd-three.vercel.app
- π€ Upload & Sell - Upload PDF documents and set your price
- π³ Crypto Payments - Secure payments with Bitcoin SV
- π Wallet Integration - Connect your BSV wallet
- ποΈ Secure Access - Only buyers can view purchased documents
- π Analytics - Track sales, revenue, and document performance
- π·οΈ Tags & Search - Organize and discover documents
- π Creator Dashboard - Detailed insights for content creators
- π¨ Modern UI - Dark mode, responsive design
- Node.js 20+
- Supabase account (sign up free)
- BSV wallet (for payments)
git clone <your-repo>
cd ppd
npm install- Create a project at supabase.com
- Go to SQL Editor in dashboard
- Run these migrations in order:
# Copy and run in Supabase SQL Editor:
backend/supabase/migrations/001_create_documents_table.sql
backend/supabase/migrations/002_create_purchases_table.sql
backend/supabase/migrations/003_create_tags_system.sql
backend/supabase/migrations/004_create_row_level_security.sqlCreate .env.local:
# Supabase (from https://app.supabase.com/project/_/settings/api)
NEXT_PUBLIC_SUPABASE_URL=your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
# Supabase Service Role Key (Required for admin operations like delete)
# IMPORTANT: Keep this secret! Never expose to client-side code
# Get from: https://app.supabase.com/project/_/settings/api (under "service_role" section)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
# Backend Wallet (generates from setup command)
BACKEND_PRIVATE_KEY=your-backend-private-key
STORAGE_URL=https://storage.babbage.systems
NETWORK=mainImportant: The SUPABASE_SERVICE_ROLE_KEY is required for document deletion to work properly. This key bypasses Row Level Security (RLS) policies and should only be used in secure server-side contexts.
npm run setup:walletThis creates a backend wallet and adds PRIVATE_KEY to .env.
npm run devOpen http://localhost:3000 π
For testing payments locally:
npm run wallet-serverββββββββββββββββββββββββββββββββββββββββββββ
β Next.js Frontend β
β βββββββββββ βββββββββββ βββββββββββ β
β β Home β β Upload β β Viewer β β
β β Page β β Page β β Page β β
β βββββββββββ βββββββββββ βββββββββββ β
ββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
ββββββββββββΌβββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββ ββββββββ ββββββββββββ
β Supabase β β BSV β β API β
β Database β β SDK β β Routes β
ββββββββββββ ββββββββ ββββββββββββ
Frontend:
- Next.js 16 (App Router) + React 19
- TypeScript + Tailwind CSS
- shadcn/ui components
- BSV SDK for wallet integration
Backend:
- Supabase (PostgreSQL database)
- Next.js API Routes
- BSV blockchain integration
- Payment & authentication middleware
ppd/
βββ app/ # Next.js pages
β βββ page.tsx # Home/marketplace
β βββ upload/page.tsx # Upload documents
β βββ view/[id]/page.tsx # View purchased docs
β βββ published/page.tsx # Creator's docs
β βββ library/page.tsx # User's purchases
β βββ creator/stats/page.tsx # Analytics
β
βββ pages/api/ # API endpoints
β βββ documents/ # CRUD + purchase
β βββ purchases/ # Purchase history
β βββ stats/ # Analytics data
β βββ tags/ # Tag management
β
βββ components/ # React components
β βββ ui/ # shadcn/ui
β βββ document-card.tsx # Document display
β βββ wallet-provider.tsx # Wallet state
β βββ wallet-button.tsx # Connect UI
β βββ upload-document.tsx # Upload form
β
βββ backend/supabase/ # Database layer
β βββ migrations/ # SQL migrations
β βββ documents.ts # Document queries
β βββ purchases.ts # Purchase queries
β βββ stats.ts # Analytics queries
β βββ tags.ts # Tag queries
β
βββ lib/ # Utilities
βββ wallet.ts # Frontend wallet
βββ wallet-server.ts # Backend wallet
βββ middleware.ts # Auth & payments
Stores document metadata and binary file data.
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| title | VARCHAR(255) | Document title |
| hash | VARCHAR(255) | SHA-256 hash (unique) |
| cost | FLOAT | Price in satoshis |
| address_owner | VARCHAR(255) | Creator's BSV address |
| file_data | BYTEA | Binary PDF data |
| file_size | INTEGER | Size in bytes |
| mime_type | VARCHAR(100) | File type |
| created_at | TIMESTAMP | Upload time |
Tracks document purchases with blockchain transactions.
| Column | Type | Description |
|---|---|---|
| id | UUID | Primary key |
| address_buyer | VARCHAR(255) | Buyer's BSV address |
| doc_id | UUID | FK to documents.id |
| transaction_id | VARCHAR(255) | BSV transaction hash |
| created_at | TIMESTAMP | Purchase time |
Many-to-many relationship for document categorization.
List/Search Documents
GET /api/documents
GET /api/documents?title=react
GET /api/documents?tags=tutorial,beginnerGet Document Details
GET /api/documents/[id]Upload Document
POST /api/documents
Content-Type: multipart/form-data
Fields:
- file: PDF file
- title: Document title
- cost: Price in satoshis
- address_owner: Creator's BSV address
- tags: JSON array of tag namesView/Download Document
GET /api/documents/[id]/view?buyer=[address]Purchase Document
POST /api/documents/[id]/purchase
Headers:
- x-bsv-payment: Payment transaction dataDelete Document
DELETE /api/documents/[id]Get User's Purchases
GET /api/purchases/buyer/[address]Creator Analytics
GET /api/stats/creator/[address]List All Tags
GET /api/tagsCreate Tag
POST /api/tags
Body: { "name": "tutorial" }The app uses two separate wallets:
Purpose: Receives payments from buyers
Setup:
npm run setup:walletThis generates a private key and saves it to .env as PRIVATE_KEY. Add it to Vercel as BACKEND_PRIVATE_KEY.
How it works:
- Runs in Next.js API routes
- Uses BSV payment middleware
- Automatically receives payments
- No separate server needed
Purpose: Users make payments from their wallet
For Development:
npm run wallet-serverThis starts a local wallet server on localhost:3321.
For Production: Users need their own BSV wallet with JSON-API support configured for your domain.
- Push to GitHub
git add .
git commit -m "Initial commit"
git push origin main-
Connect to Vercel
- Go to vercel.com
- Import your GitHub repository
- Vercel auto-detects Next.js
-
Add Environment Variables
In Vercel dashboard β Settings β Environment Variables:
NEXT_PUBLIC_SUPABASE_URL=your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
BACKEND_PRIVATE_KEY=your-backend-private-key
STORAGE_URL=https://storage.babbage.systems
NETWORK=main
- Deploy
vercel --prodRun in Supabase SQL Editor:
-- Enable RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
ALTER TABLE purchases ENABLE ROW LEVEL SECURITY;
ALTER TABLE tags ENABLE ROW LEVEL SECURITY;
ALTER TABLE document_tags ENABLE ROW LEVEL SECURITY;
-- Public read policies
CREATE POLICY "Public read documents" ON documents FOR SELECT USING (true);
CREATE POLICY "Public read purchases" ON purchases FOR SELECT USING (true);
CREATE POLICY "Public read tags" ON tags FOR SELECT USING (true);
CREATE POLICY "Public read document_tags" ON document_tags FOR SELECT USING (true);
-- Public write policies
CREATE POLICY "Anyone can upload" ON documents FOR INSERT WITH CHECK (true);
CREATE POLICY "Anyone can purchase" ON purchases FOR INSERT WITH CHECK (true);
CREATE POLICY "Anyone can create tags" ON tags FOR INSERT WITH CHECK (true);
CREATE POLICY "Anyone can tag documents" ON document_tags FOR INSERT WITH CHECK (true);npm run dev # Start dev server (localhost:3000)
npm run build # Build for production
npm run start # Start production server
npm run lint # Run ESLint
npm run setup:wallet # Generate backend wallet
npm run wallet-server # Start local wallet server# Terminal 1: Main app
npm run dev
# Terminal 2: Wallet server (for testing payments)
npm run wallet-server
# Terminal 3: Watch logs
# Check console for errors# List documents
curl http://localhost:3000/api/documents
# Upload document
curl -X POST http://localhost:3000/api/documents \
-F "[email protected]" \
-F "title=My Document" \
-F "cost=1000" \
-F "address_owner=03abc..." \
-F 'tags=["tutorial","react"]'
# Search documents
curl "http://localhost:3000/api/documents?title=react&tags=tutorial"
# Get wallet info
curl http://localhost:3000/api/wallet-info- Start wallet server:
npm run wallet-server - Visit app:
http://localhost:3000 - Click "Connect Wallet"
- Should see green badge with wallet address
Problem: 500 error when loading documents
Solution:
- Check
NEXT_PUBLIC_SUPABASE_URLin.env.local - Verify
NEXT_PUBLIC_SUPABASE_ANON_KEY - Ensure migrations are run
- Check RLS policies are created
Problem: "No wallet detected" error
Solution:
- Make sure wallet server is running:
npm run wallet-server - Check port 3321 is not blocked
- Verify CORS is configured (for deployed app)
- Look at browser console for detailed errors
Problem: Upload returns error
Solution:
- Check file is PDF format
- Verify file size < 50MB
- Ensure
file_datacolumn exists in database - Check backend wallet is configured
Problem: "Wallet not connected" on view page
Solution:
- Connect wallet first (top-right button)
- Wallet must be running:
npm run wallet-server - Check wallet address matches document owner
Solution:
- Check terminal logs:
npm run dev - Verify all environment variables are set
- Test Supabase connection
- Check API route imports
- User clicks "Purchase" on document
- Frontend requests payment info from backend
- Backend returns payment derivation prefix
- User's wallet creates payment transaction
- Frontend sends signed transaction to backend
- Backend payment middleware validates
- Backend wallet receives payment
- Purchase record created in database
- User can now view document
- Document Owner: Can view anytime
- Buyers: Can view after purchasing
- Others: Must purchase first
- Access verified via BSV wallet address
- Documents support multiple tags
- Search by title and/or tags
- Case-insensitive matching
- Auto-complete suggestions
- Files stored as binary (BYTEA) in database
- SHA-256 hash for integrity
- Served securely through API
- Access control via wallet verification
- Total documents published
- Total sales and revenue
- Daily sales charts
- Top performing documents
- Filter by date range
- Upload PDF documents
- Set price in satoshis
- Add tags for categorization
- Generate shareable payment links
- Delete own documents
- Browse all documents
- Search by title
- Filter by tags
- View document details
- Purchase with BSV
- My Library view for purchases
- Auto-connect on app load
- Connect/disconnect manually
- Persistent session
- Transaction signing
- Payment verification
NEXT_PUBLIC_WALLET_HOST=localhost:3001NETWORK=test # For testnet
NETWORK=main # For mainnetSTORAGE_URL=https://your-storage-provider.comThe creator dashboard provides:
- Total Documents: Number of published docs
- Total Purchases: Total sales count
- Total Revenue: Earnings in satoshis
- Average Price: Mean document price
- Daily Stats: Sales over time chart
- Top Documents: Best performing content
This is a personal project, but suggestions are welcome!
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
The app now uses react-pdf (built on PDF.js) for better PDF rendering instead of iframes. Here are the different approaches:
Pros:
- β¨ Native React component
- π¨ Full styling control
- π± Better mobile support
- π Built-in zoom and navigation
- π Page-by-page rendering
- π― Text selection and search
- π More secure (no iframe sandboxing issues)
Cons:
- π¦ Larger bundle size (~500KB)
- π Requires worker configuration
<iframe src={pdfUrl} className="w-full h-full" />- β Simple, native browser support
- β Limited control over UI
- β Browser compatibility issues
- β No zoom/navigation controls
<object data={pdfUrl} type="application/pdf" />- β Native HTML element
- β Similar limitations to iframe
- β Poor mobile support
// More control but more complex setup
import * as pdfjsLib from 'pdfjs-dist';
// Manual canvas rendering- β Maximum control
- β More complex implementation
- β Requires manual UI building
- PDF.js Express (commercial)
- PSPDFKit (commercial)
- β Enterprise features
- β Licensing costs
The app uses the unpkg CDN for the PDF.js worker:
pdfjs.GlobalWorkerOptions.workerSrc =
`//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;For production, consider self-hosting the worker file for better performance and reliability.
If you prefer the iframe approach, simply replace the PDFViewer component in app/view/[id]/page.tsx:
// Instead of:
<PDFViewer url={pdfUrl} title={documentMetadata?.title} onDownload={handleDownload} />
// Use:
<div className="w-full h-full">
<iframe src={pdfUrl} className="w-full h-full border-0" title={documentMetadata?.title || 'Document'} />
</div>If documents aren't displaying properly in the viewer page, check the following:
Open the browser console (F12) and look for:
- "PDF magic bytes check" - Should show
%PDF(indicates valid PDF data) - "Received blob" - Should show file size and type
- "PDF header check" - Should show
%PDF-
The PDF files are stored as BYTEA in Supabase. Check:
# In Supabase SQL Editor, check a document's file_data:
SELECT id, title, file_size,
length(file_data) as stored_bytes,
substring(file_data, 1, 4) as file_header
FROM documents
LIMIT 1;stored_bytesshould matchfile_sizefile_headershould show hex bytes starting with25504446(which is %PDF in hex)
Test the view API endpoint:
# Check if the API returns valid PDF data
curl -v "http://localhost:3000/api/documents/[DOC_ID]/view?buyer=[WALLET_ADDRESS]" > test.pdf
# Verify it's a valid PDF
file test.pdf # Should show "PDF document"Some browsers don't support inline PDF viewing in iframes:
- β Chrome/Edge: Full support
β οΈ Firefox: May require PDF.jsβ οΈ Safari: Limited iframe PDF support- π‘ The app now includes a fallback download option for unsupported browsers
Issue: "Received empty file data"
- Cause: Document uploaded but file_data is NULL or empty
- Fix: Re-upload the document
Issue: "Invalid PDF file format"
- Cause: File data corrupted during storage/retrieval
- Fix: Check the hex string conversion in
backend/supabase/documents.ts - Verify:
\\xprefix is correct for BYTEA hex format
Issue: "Failed to display PDF"
- Cause: Browser security restrictions
- Fix: Use the download button instead
Check the following files for detailed console logs:
app/view/[id]/page.tsx- Frontend PDF loadingpages/api/documents/[id]/view.ts- API endpointbackend/supabase/document-files.ts- Database retrieval
# 1. Upload a simple test PDF
# 2. Check the console logs during upload
# 3. Navigate to the viewer page
# 4. Check the console logs during viewing
# 5. Compare file sizes: upload vs download- Check that your Supabase database migrations ran successfully
- Verify Row Level Security policies allow document access
- Ensure your BSV wallet is properly connected
- Try clearing browser cache and reloading
MIT License - Feel free to use this project for your own purposes.
Built with:
- Next.js - React framework
- Supabase - PostgreSQL database
- BSV SDK - Bitcoin SV integration
- shadcn/ui - UI components
- Tailwind CSS - Styling
- π Check code comments for implementation details
- π Open GitHub issues for bugs
- π‘ Feature requests welcome
- π§ Contact via GitHub
Built with β€οΈ using Next.js, Bitcoin SV, and Supabase