Initial commit
This commit is contained in:
837
specs.md
Normal file
837
specs.md
Normal file
@@ -0,0 +1,837 @@
|
||||
# Paid Link Paywall Platform
|
||||
|
||||
Goal
|
||||
Build a product that lets creators turn any link into paid access in under 60 seconds, and embed the paywall on any website. Users do not need to know Nostr exists. No custody of user funds.
|
||||
|
||||
Core one-liner
|
||||
Paste a link, set a price, share or embed, get paid.
|
||||
|
||||
Non-goals for v1
|
||||
|
||||
* No community features (posts, comments, member feeds)
|
||||
* No file hosting (unless optional later)
|
||||
* No subscription memberships (one-time purchase only in v1)
|
||||
* No complex analytics or funnels
|
||||
* No marketplace or discovery directory
|
||||
|
||||
Key principles
|
||||
|
||||
* Fastest possible creator onboarding: 1 minute to first paywall
|
||||
* Buyer friction is minimal: open link, pay, unlock
|
||||
* Works with existing content locations (Notion, Google Docs, PDFs, Loom, YouTube unlisted, private web pages, GitHub, etc.)
|
||||
* Embed-first product: creators can drop into Webflow, WordPress, Framer, custom sites
|
||||
* Security is token-based and server-verified
|
||||
* Payment is non-custodial: platform only checks payment status and grants access
|
||||
|
||||
---
|
||||
|
||||
## Product surfaces
|
||||
|
||||
1. Creator web app
|
||||
|
||||
* Landing page
|
||||
* Auth
|
||||
* Creator dashboard
|
||||
* Create paywall flow
|
||||
* Paywall detail page
|
||||
* Sales list
|
||||
* Payouts and settings
|
||||
* Embed docs page
|
||||
|
||||
2. Buyer paywall pages
|
||||
|
||||
* Hosted paywall page (shareable)
|
||||
* Embedded paywall UI (iframe)
|
||||
* Embedded button + checkout modal (script)
|
||||
|
||||
3. Developer API
|
||||
|
||||
* Create paywall
|
||||
* Update paywall
|
||||
* List paywalls
|
||||
* Create checkout session
|
||||
* Verify access token
|
||||
* Webhooks
|
||||
|
||||
---
|
||||
|
||||
## Roles and permissions
|
||||
|
||||
Roles
|
||||
|
||||
* Creator (default)
|
||||
* Admin
|
||||
|
||||
Creator can
|
||||
|
||||
* Create/update/archive paywalls
|
||||
* View sales
|
||||
* Configure payout destination
|
||||
* Generate embed code
|
||||
* Configure access rules
|
||||
|
||||
Admin can
|
||||
|
||||
* View all creators
|
||||
* View paywalls
|
||||
* View sales
|
||||
* Manage disputes and refunds references (actual refunds depend on payment provider)
|
||||
* Disable creators or paywalls
|
||||
* Configure global settings
|
||||
|
||||
---
|
||||
|
||||
## Data model
|
||||
|
||||
Entities
|
||||
|
||||
Creator
|
||||
|
||||
* id (uuid)
|
||||
* created_at
|
||||
* status (active, disabled)
|
||||
* display_name
|
||||
* email (optional if using email login)
|
||||
* auth_providers (password, nostr, google, github)
|
||||
* nostr_pubkey (optional)
|
||||
* avatar_url
|
||||
* default_currency (sats)
|
||||
* payout_config (provider-specific)
|
||||
* tax_info (future)
|
||||
|
||||
Paywall
|
||||
|
||||
* id (uuid)
|
||||
* creator_id
|
||||
* status (active, archived, disabled)
|
||||
* created_at, updated_at
|
||||
* title
|
||||
* description (short)
|
||||
* cover_image_url (optional)
|
||||
* original_url (the target link)
|
||||
* original_url_type (url, youtube, notion, pdf, loom, other)
|
||||
* preview_mode (none, text_preview, image_preview)
|
||||
* preview_content (optional)
|
||||
* price_sats (integer)
|
||||
* access_expiry_seconds (optional; null means no expiry)
|
||||
* max_devices (optional; default 3)
|
||||
* max_sessions (optional; default 5)
|
||||
* allow_embed (boolean)
|
||||
* allowed_embed_origins (list of domains, optional)
|
||||
* require_email_receipt (boolean)
|
||||
* custom_success_message (optional)
|
||||
* custom_branding (theme overrides, optional)
|
||||
* slug (optional human friendly)
|
||||
|
||||
CheckoutSession
|
||||
|
||||
* id (uuid)
|
||||
* paywall_id
|
||||
* created_at
|
||||
* status (pending, paid, expired, canceled)
|
||||
* amount_sats
|
||||
* payment_provider (lnbits, openNode, strike, etc)
|
||||
* payment_request (invoice string or provider id)
|
||||
* expires_at
|
||||
* buyer_hint (ip hash, user agent hash)
|
||||
|
||||
AccessGrant
|
||||
|
||||
* id (uuid)
|
||||
* paywall_id
|
||||
* created_at
|
||||
* expires_at (nullable)
|
||||
* status (active, revoked)
|
||||
* buyer_id (nullable)
|
||||
* token_id (uuid)
|
||||
* last_used_at
|
||||
* usage_count
|
||||
|
||||
Buyer
|
||||
|
||||
* id (uuid)
|
||||
* created_at
|
||||
* email (optional)
|
||||
* email_verified (boolean)
|
||||
* nostr_pubkey (optional)
|
||||
* notes (optional)
|
||||
|
||||
AccessToken
|
||||
|
||||
* token_id (uuid)
|
||||
* paywall_id
|
||||
* buyer_id (nullable)
|
||||
* issued_at
|
||||
* expires_at (nullable)
|
||||
* scopes (view)
|
||||
* signature (server signed)
|
||||
|
||||
Sale
|
||||
|
||||
* id (uuid)
|
||||
* paywall_id
|
||||
* creator_id
|
||||
* created_at
|
||||
* amount_sats
|
||||
* platform_fee_sats
|
||||
* net_sats
|
||||
* payment_provider
|
||||
* provider_reference
|
||||
* buyer_id (nullable)
|
||||
* status (confirmed, refunded, chargeback, disputed)
|
||||
|
||||
WebhookEvent
|
||||
|
||||
* id
|
||||
* created_at
|
||||
* provider
|
||||
* event_type
|
||||
* raw_payload
|
||||
* processed_at
|
||||
* status
|
||||
|
||||
AuditLog
|
||||
|
||||
* id
|
||||
* created_at
|
||||
* actor (creator/admin/system)
|
||||
* action
|
||||
* resource_type
|
||||
* resource_id
|
||||
* ip
|
||||
* metadata
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Creator login options
|
||||
|
||||
* Email + password
|
||||
* Nostr login (NIP-07 or signer)
|
||||
* Google OAuth
|
||||
* GitHub OAuth
|
||||
|
||||
Buyer login
|
||||
|
||||
* No account required for purchase
|
||||
* Optional email input for receipt and re-access
|
||||
* Optional Nostr login for portable access later
|
||||
|
||||
Session management
|
||||
|
||||
* JWT access token (short)
|
||||
* Refresh token (httpOnly cookie)
|
||||
* Rate limiting on auth endpoints
|
||||
* 2FA is future
|
||||
|
||||
Nostr integration
|
||||
|
||||
* If creator uses Nostr login: store pubkey and verify signed challenge.
|
||||
* If creator uses email login: system creates and stores a backend keypair to sign certain actions internally (never shown to user). This key is per-creator and encrypted at rest.
|
||||
|
||||
---
|
||||
|
||||
## Payments
|
||||
|
||||
v1 payment methods
|
||||
|
||||
* Lightning only (invoice-based)
|
||||
|
||||
Implementation options
|
||||
|
||||
* LNbits as payment backend
|
||||
* Alternative provider module interface to swap later
|
||||
|
||||
Important constraints
|
||||
|
||||
* Platform does not custody funds: invoice is generated for creator payout wallet or a routed invoice to platform then forwarded is not allowed.
|
||||
|
||||
Recommended model
|
||||
|
||||
* Platform maintains a service wallet only for collecting platform fees.
|
||||
* Creator config includes their own Lightning Address or LNURLp.
|
||||
* For each checkout, platform generates:
|
||||
|
||||
* one invoice to creator for net amount
|
||||
* one invoice to platform for fee
|
||||
* or a split payment mechanism if supported by provider.
|
||||
|
||||
If split is not supported
|
||||
|
||||
* v1 can temporarily charge a platform fee via subscription model instead of per-sale fee to avoid custodial flow.
|
||||
|
||||
Fee model
|
||||
|
||||
* Free tier: 10 percent per sale
|
||||
* Pro tier: 15 per month + 3 percent per sale
|
||||
|
||||
---
|
||||
|
||||
## Core flows
|
||||
|
||||
### Flow A: Creator creates a paywall
|
||||
|
||||
Entry points
|
||||
|
||||
* Dashboard primary CTA: Create Paywall
|
||||
* Top nav button
|
||||
|
||||
Steps
|
||||
|
||||
1. Paste URL
|
||||
2. System fetches metadata:
|
||||
|
||||
* page title
|
||||
* description
|
||||
* favicon
|
||||
* open graph image
|
||||
3. Creator edits:
|
||||
|
||||
* title
|
||||
* short description
|
||||
* cover image
|
||||
4. Set price
|
||||
|
||||
* sats integer
|
||||
5. Set access rules
|
||||
|
||||
* expiry: none / 24h / 7d / 30d / custom
|
||||
* max devices: default 3
|
||||
* allow embed: on
|
||||
* allowed origins: optional list
|
||||
6. Generate output
|
||||
|
||||
* Share link
|
||||
* Embed code (iframe)
|
||||
* Button embed code
|
||||
7. Save
|
||||
|
||||
Success state
|
||||
|
||||
* Show the paywall URL
|
||||
* Copy buttons
|
||||
* Preview mode
|
||||
|
||||
Validation
|
||||
|
||||
* URL must be https
|
||||
* Block localhost and private IP ranges
|
||||
* Block obvious malware/phishing domains (basic allow/deny list)
|
||||
|
||||
---
|
||||
|
||||
### Flow B: Buyer purchases access via hosted paywall page
|
||||
|
||||
Entry
|
||||
|
||||
* buyer opens [https://app.domain/p/{slug_or_id}](https://app.domain/p/{slug_or_id})
|
||||
|
||||
Paywall page sections
|
||||
|
||||
* cover image
|
||||
* title
|
||||
* short description
|
||||
* price
|
||||
* what you get (simple)
|
||||
* pay button
|
||||
* trust line (secure payment)
|
||||
|
||||
When pay is clicked
|
||||
|
||||
* create checkout session
|
||||
* render QR invoice
|
||||
* show invoice string copy
|
||||
* show countdown timer
|
||||
* poll payment status every 2 seconds, max 2 minutes
|
||||
* on paid:
|
||||
|
||||
* issue access token
|
||||
* redirect to unlocked view
|
||||
|
||||
Unlocked view
|
||||
|
||||
* show success message
|
||||
* show button: Open content
|
||||
* auto redirect after 3 seconds
|
||||
* store access token in secure cookie and localStorage token id (for iframe context)
|
||||
|
||||
Re-access
|
||||
|
||||
* if cookie exists and token valid: show Open content
|
||||
* if token expired: show repurchase prompt
|
||||
|
||||
---
|
||||
|
||||
### Flow C: Embedded iframe paywall
|
||||
|
||||
Creator embeds
|
||||
|
||||
<iframe src="https://app.domain/embed/{paywall_id}" ...></iframe>
|
||||
|
||||
States
|
||||
|
||||
* Loading state
|
||||
* Locked state
|
||||
* Checkout state (inside iframe)
|
||||
* Unlocked state
|
||||
|
||||
Locked state UI
|
||||
|
||||
* small cover
|
||||
* title
|
||||
* price
|
||||
* Unlock button
|
||||
|
||||
Unlock
|
||||
|
||||
* opens checkout inside iframe
|
||||
* after paid, iframe switches to unlocked
|
||||
|
||||
Unlocked state behaviors
|
||||
Option 1: reveal content within iframe by rendering a secure redirect button
|
||||
Option 2: open target link in new tab with token parameter (less secure)
|
||||
Option 3: show an inline preview plus open link button
|
||||
|
||||
Recommended v1
|
||||
|
||||
* Open target link in new tab after payment, and show persistent “Open again” button.
|
||||
|
||||
Origin restrictions
|
||||
|
||||
* If creator set allowed origins, embed endpoint checks the request referrer/origin.
|
||||
* If mismatch: show error: embedding not allowed.
|
||||
|
||||
---
|
||||
|
||||
### Flow D: Button embed + modal checkout
|
||||
|
||||
Embed snippet
|
||||
|
||||
<script src="https://app.domain/js/paywall.js" data-paywall="{id}" data-theme="auto"></script>
|
||||
|
||||
Behavior
|
||||
|
||||
* Script finds placeholder element or injects a button.
|
||||
* On click, opens modal overlay.
|
||||
* Modal shows paywall details and invoice.
|
||||
* On paid, modal shows success and “Open content” button.
|
||||
|
||||
Constraints
|
||||
|
||||
* Must be CSP-friendly
|
||||
* Must not break SSR sites
|
||||
* Provide a no-JS fallback link
|
||||
|
||||
---
|
||||
|
||||
### Flow E: Creator revenue and sales tracking
|
||||
|
||||
Dashboard widgets
|
||||
|
||||
* Total revenue (last 7d, 30d)
|
||||
* Net earnings
|
||||
* Top paywalls
|
||||
* Sales count
|
||||
|
||||
Sales table
|
||||
|
||||
* date
|
||||
* paywall
|
||||
* amount
|
||||
* fee
|
||||
* net
|
||||
* status
|
||||
|
||||
Filters
|
||||
|
||||
* date range
|
||||
* paywall
|
||||
|
||||
Exports
|
||||
|
||||
* CSV export v1 optional
|
||||
|
||||
---
|
||||
|
||||
## Pages and UI spec
|
||||
|
||||
### Public landing page
|
||||
|
||||
Sections
|
||||
|
||||
* Hero headline: “Turn any link into paid access in 60 seconds.”
|
||||
* Subheadline: “No uploads. No platform lock-in. Share or embed anywhere.”
|
||||
* CTA: “Create a paywall”
|
||||
* Demo embed showing locked/unlocked
|
||||
* Use cases grid (Notion, Google Docs, PDFs, videos)
|
||||
* Pricing
|
||||
* FAQ
|
||||
* Footer
|
||||
|
||||
Design
|
||||
|
||||
* Modern, premium, minimal
|
||||
* Dark mode first with light mode toggle
|
||||
* Rounded corners, soft shadows
|
||||
* Strong typography
|
||||
* No crypto jargon
|
||||
|
||||
### Auth pages
|
||||
|
||||
* Login
|
||||
* Signup
|
||||
* Provider buttons (Google, GitHub, Nostr)
|
||||
* Forgot password
|
||||
|
||||
### Creator dashboard
|
||||
|
||||
Layout
|
||||
|
||||
* Left sidebar
|
||||
|
||||
* Overview
|
||||
* Paywalls
|
||||
* Sales
|
||||
* Embeds
|
||||
* Settings
|
||||
* Help
|
||||
* Top bar
|
||||
|
||||
* Create Paywall
|
||||
* Profile menu
|
||||
|
||||
Overview page
|
||||
|
||||
* KPI cards
|
||||
* Sales chart (simple line)
|
||||
* Recent sales list
|
||||
* Quick create CTA
|
||||
|
||||
Paywalls list
|
||||
|
||||
* Table / cards
|
||||
* Status chips
|
||||
* Copy link
|
||||
* Copy embed
|
||||
* Edit
|
||||
* Archive
|
||||
|
||||
Create paywall page
|
||||
|
||||
* Stepper UI (Paste link -> Details -> Price -> Access -> Output)
|
||||
|
||||
Paywall detail page
|
||||
|
||||
* Preview hosted page
|
||||
* Settings panels
|
||||
* Copy buttons
|
||||
* Embed origins list
|
||||
* Regenerate embed codes
|
||||
|
||||
Sales page
|
||||
|
||||
* Sales table
|
||||
* Export
|
||||
|
||||
Embeds page
|
||||
|
||||
* Docs and copy-paste snippets
|
||||
* Testing sandbox
|
||||
|
||||
Settings
|
||||
|
||||
* Profile
|
||||
* Payout destination
|
||||
* Branding (logo, primary color)
|
||||
* Domain/whitelabel (future)
|
||||
* Security (sessions)
|
||||
|
||||
### Buyer hosted paywall page
|
||||
|
||||
* Responsive
|
||||
* Minimal checkout
|
||||
* QR centered
|
||||
* Clear instructions
|
||||
* After pay: success state
|
||||
|
||||
---
|
||||
|
||||
## Look and feel
|
||||
|
||||
Theme
|
||||
|
||||
* Primary color: choose one brand color; allow creator override later
|
||||
* Background: dark #0b0f14 style
|
||||
* Cards: slightly lighter panels
|
||||
* Typography: modern sans
|
||||
* Spacing: generous
|
||||
|
||||
Components
|
||||
|
||||
* Buttons: large, rounded, high contrast
|
||||
* Inputs: simple
|
||||
* Toasts for copy actions
|
||||
* Skeleton loaders
|
||||
|
||||
Microcopy tone
|
||||
|
||||
* Clear, short, confident
|
||||
* Avoid crypto words
|
||||
* Use “sats” not BTC
|
||||
|
||||
Accessibility
|
||||
|
||||
* AA contrast
|
||||
* keyboard navigable
|
||||
* screen reader labels
|
||||
|
||||
---
|
||||
|
||||
## Backend architecture
|
||||
|
||||
Recommended stack
|
||||
|
||||
* FastAPI or Node/Express
|
||||
* Postgres
|
||||
* Redis for session and rate limit
|
||||
* Worker queue for webhook processing
|
||||
|
||||
Services
|
||||
|
||||
1. API service
|
||||
|
||||
* REST endpoints
|
||||
* auth
|
||||
* paywall CRUD
|
||||
* checkout sessions
|
||||
* access verification
|
||||
|
||||
2. Webhook worker
|
||||
|
||||
* provider webhooks
|
||||
* marks CheckoutSession paid
|
||||
* issues AccessGrant
|
||||
* records Sale
|
||||
|
||||
3. Embed service
|
||||
|
||||
* serves iframe content
|
||||
* verifies origin
|
||||
* uses access token
|
||||
|
||||
Security
|
||||
|
||||
* Strict input validation
|
||||
* SSRF protection on metadata fetch
|
||||
* Rate limits
|
||||
* IP throttling on checkout creation
|
||||
* Signed tokens (JWT with rotating keys)
|
||||
* Encrypt secrets at rest
|
||||
|
||||
Metadata fetcher
|
||||
|
||||
* Fetch open graph tags
|
||||
* Timeout 3 seconds
|
||||
* Only allow https
|
||||
* Block private ranges
|
||||
|
||||
---
|
||||
|
||||
## API endpoints (v1)
|
||||
|
||||
Auth
|
||||
|
||||
* POST /auth/signup
|
||||
* POST /auth/login
|
||||
* POST /auth/logout
|
||||
* POST /auth/refresh
|
||||
* POST /auth/oauth/{provider}/start
|
||||
* POST /auth/oauth/{provider}/callback
|
||||
* POST /auth/nostr/challenge
|
||||
* POST /auth/nostr/verify
|
||||
|
||||
Paywalls
|
||||
|
||||
* POST /paywalls
|
||||
* GET /paywalls
|
||||
* GET /paywalls/{id}
|
||||
* PATCH /paywalls/{id}
|
||||
* POST /paywalls/{id}/archive
|
||||
|
||||
Checkout
|
||||
|
||||
* POST /paywalls/{id}/checkout
|
||||
* GET /checkout/{session_id}
|
||||
|
||||
Access
|
||||
|
||||
* POST /access/verify
|
||||
* POST /access/revoke
|
||||
|
||||
Public
|
||||
|
||||
* GET /p/{slug_or_id} (render)
|
||||
* GET /embed/{id} (render)
|
||||
* GET /js/paywall.js (button script)
|
||||
|
||||
Webhooks
|
||||
|
||||
* POST /webhooks/{provider}
|
||||
|
||||
Admin
|
||||
|
||||
* GET /admin/creators
|
||||
* GET /admin/paywalls
|
||||
* GET /admin/sales
|
||||
* POST /admin/paywalls/{id}/disable
|
||||
|
||||
---
|
||||
|
||||
## Token and access logic
|
||||
|
||||
On payment confirmed
|
||||
|
||||
* Create AccessGrant
|
||||
* Issue AccessToken JWT
|
||||
Claims
|
||||
* token_id
|
||||
* paywall_id
|
||||
* buyer_id (optional)
|
||||
* iat
|
||||
* exp (optional)
|
||||
|
||||
Storage
|
||||
|
||||
* httpOnly cookie: access_jwt
|
||||
* localStorage: token_id (for embed communication)
|
||||
|
||||
Verification
|
||||
|
||||
* /access/verify checks signature and status
|
||||
* checks revoked
|
||||
* checks expiry
|
||||
* checks max device fingerprint count
|
||||
|
||||
Device fingerprint
|
||||
|
||||
* hash of user agent + stable client id cookie
|
||||
* do not use invasive fingerprinting
|
||||
|
||||
Limits
|
||||
|
||||
* max devices default 3
|
||||
* if exceeded: prompt to “Reset devices” (creator can allow)
|
||||
|
||||
---
|
||||
|
||||
## Abuse prevention
|
||||
|
||||
Threats
|
||||
|
||||
* Link sharing
|
||||
* Invoice replay
|
||||
* Token theft
|
||||
* Bots spamming checkout sessions
|
||||
* SSRF via URL fetch
|
||||
|
||||
Controls
|
||||
|
||||
* Access tokens tied to limited devices
|
||||
* Session expiry and revocation
|
||||
* Rate limit checkout creation
|
||||
* Webhook idempotency
|
||||
* Block private network URL fetch
|
||||
|
||||
---
|
||||
|
||||
## Observability and operations
|
||||
|
||||
Logging
|
||||
|
||||
* request id
|
||||
* creator id
|
||||
* paywall id
|
||||
* checkout session id
|
||||
* webhook event id
|
||||
|
||||
Metrics
|
||||
|
||||
* checkouts created
|
||||
* conversion rate
|
||||
* webhook latency
|
||||
* embed loads
|
||||
* errors
|
||||
|
||||
Alerts
|
||||
|
||||
* webhook failures
|
||||
* payment provider downtime
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
Environments
|
||||
|
||||
* dev
|
||||
* staging
|
||||
* production
|
||||
|
||||
Secrets
|
||||
|
||||
* provider keys
|
||||
* jwt signing keys
|
||||
* db password
|
||||
|
||||
CDN
|
||||
|
||||
* cache static assets
|
||||
* do not cache paywall pages with personal state
|
||||
|
||||
---
|
||||
|
||||
## v1 acceptance criteria
|
||||
|
||||
Creator
|
||||
|
||||
* Can sign up and create a paywall in under 60 seconds
|
||||
* Can copy share link and embed code
|
||||
* Can see sales appear after payments
|
||||
|
||||
Buyer
|
||||
|
||||
* Can pay via lightning invoice and unlock within seconds
|
||||
* Can revisit link and still access if not expired
|
||||
|
||||
Embed
|
||||
|
||||
* Iframe works on standard websites
|
||||
* Button script opens modal checkout
|
||||
|
||||
Security
|
||||
|
||||
* Tokens are signed and validated server-side
|
||||
* Metadata fetch is protected against SSRF
|
||||
|
||||
---
|
||||
|
||||
## Roadmap after v1
|
||||
|
||||
v1.1
|
||||
|
||||
* Email receipts
|
||||
* Custom success message
|
||||
* Better origin controls
|
||||
|
||||
v1.2
|
||||
|
||||
* Whitelabel domains
|
||||
* Team accounts
|
||||
|
||||
v2
|
||||
|
||||
* Subscriptions
|
||||
* Bundles
|
||||
* Creator pages
|
||||
* Affiliate links
|
||||
Reference in New Issue
Block a user