- Production: easyebookgiveaways.com (GitHub Pages;
public/CNAME). - Alternate URL:
https://monapdx.github.io/easy-ebook-giveaways/may redirect to the apex domain; GitHub Pages “project URL only” builds needVITE_BASE_PATHset to/easy-ebook-giveaways/(seevite.config.jsand.github/workflows/deploy.yml). - Client routing: The app uses React Router with
HashRouter. Deep links must include the hash, for examplehttps://easyebookgiveaways.com/#/login, not/login(static hosting has no server fallback for non-hash paths). - Marketing home loads at
#/(pathname/inside the hash router). The author dashboard lives under#/app(not at the site root). - Legacy paths: Old bookmarks like
/#/campaigns/...are redirected to#/app/campaigns/...viaLegacyAppPathRedirect.
- Node: 20.x (matches CI).
- Install:
npm ciornpm install - Dev server:
npm run dev - Env: set
VITE_SUPABASE_URLandVITE_SUPABASE_ANON_KEYfor the Supabase project (Vite exposesimport.meta.env.*). - Production build:
npm run build— optionalVITE_BASE_PATHfor subdirectory deploys; the build script copiesdist/index.htmltodist/404.htmlfor GitHub Pages.
- Created app using React + Vite
- Organized project into modular feature-based architecture
- Implemented routing with React Router (
HashRouterinsrc/app/router.jsx)
-
Built reusable UI components (including Button, Input, Card, SectionHeader, Textarea, EmptyState)
-
Established layout system:
- Marketing layout — public homepage at
#/ - Dashboard layout — authenticated author app under
#/app - Public layout — giveaway and legal pages
- Marketing layout — public homepage at
- Created Supabase project
- Added environment variables (
VITE_SUPABASE_URL,VITE_SUPABASE_ANON_KEY) - Initialized client (
src/lib/supabaseClient.js)
- Login (
#/login), register (#/registerand#/signup, same screen) - Session handling via
useAuth/ Supabase session listeners - After login, navigation to the prior protected route or
#/app(AuthForm)
- Created
AuthGuard - Protected dashboard routes under
#/app - Redirects unauthenticated users to
/login(hash route:#/login)
profilescampaignslanding_pagesebooksentriesdownload_tokens
- Campaigns belong to users
- Ebooks belong to campaigns
- Entries belong to campaigns
- Download tokens link entries + ebooks
-
Enabled RLS on all tables
-
Added policies for:
- Authenticated user ownership (campaigns, ebooks)
- Public entry submission
- Token access control
- Form to create campaigns
- Stored in Supabase
- Auto-generated or user-defined slug
- New rows are created with
status: 'published'by default (createCampaign); there is no separate draft toggle in the dashboard yet
- Fetch campaigns from DB
- Display in dashboard
- View campaign details
- Added section for ebook upload
- Delete campaign is implemented in
campaignService(storage cleanup for ebook files/covers)
- Public route:
#/g/:slug
- Replaced mock data with Supabase queries
- Loaded campaign by slug
-
Captures:
- Name
- Newsletter consent
-
Stores entries in database
- Created
ebook-filesbucket - Configured storage policies
- Upload file to storage
- Save metadata to
ebookstable
-
Restricted uploads to:
.pdf.epub
-
Handled MIME type (
application/epub+zip)
- Ebook upload form tied to campaign
- Ebook stored per campaign
- Optional ebook cover upload and public URL handling for giveaway/download UI
- Created
download_tokenstable - Each entry generates a unique token
-
Linked to:
- Campaign
- Entry
- Ebook
-
Includes:
- Expiration (24 hours)
- Download limit (3)
- Download count tracking
- User submits entry
- Token created automatically
- Redirect to
#/download/:token
resolve-downloadEdge Function validates the download token server-side- Checks expiration and download limits before issuing a signed storage URL
- Increments download count on the server
- Signed URLs are created inside Edge Functions using the service role (not in the browser)
- After a successful giveaway entry, the app invokes
send-download-emailto issue/reuse a token and send the message - The project also includes
send-ebook-emailfor token-based resend/lock-aware delivery paths - Delivery is handled server-side via Supabase Edge Functions, with migration-backed tracking fields (
email_sent_at,email_send_locked_at) and lock RPC support (try_lock_download_email_send) - Best-effort per-IP rate limiting is implemented in
send-ebook-email(sliding window)
- Readers are still redirected to
#/download/:tokenso they can download immediately even if email is delayed
- RLS policies for all core tables
- Token expiration + limits
- Private storage bucket
- Signed URLs issued from Edge Functions (not the React client)
- Download token validation and signed URL creation off the client (
resolve-download) - Transactional download email via Edge Functions (
send-download-email/send-ebook-email), with idempotency + lock columns (after migration)
- Tighten storage policies further (least privilege beyond signed URLs)
- Optional: move rate limiting to a durable store (Redis / gateway) for multi-region consistency
The app now supports:
- Author authentication (login + register)
- Campaign creation and management
- Campaign-level dashboard with overview stats (
#/app,#/app/campaigns,#/app/campaigns/:campaignId) - Campaign design editing + preview (
#/app/campaigns/:campaignId/design) — Save updates in-session preview; persistence to the DB for landing fields is still a roadmap item (see Next Steps) - Campaign entries management (
#/app/campaigns/:campaignId/entries) - Campaign analytics summary cards (
#/app/campaigns/:campaignId/analytics) - Public giveaway pages (
#/g/:slug) - Success route for giveaway submissions (
#/g/:slug/success) - Ebook upload and storage
- Optional ebook cover upload and rendering on giveaway/download pages
- Entry collection
- Token-based gated downloads
- Download tracking and limits
- Download link emails via Edge Functions (DB-backed recipient lookup)
- Privacy route (
#/privacy) in public layout
🎉 Fully functional ebook giveaway pipeline
Flow:
Author → creates campaign → uploads ebook → publishes User → visits page → submits form → redirect to download + optional email with the same link
-
Run Supabase migrations in timestamp order under
supabase/migrations/:20260426140000_download_tokens_email_tracking.sql20260426150000_entries_consent_author_contact.sql20260503120000_ebook_cover_image.sql20260503121000_public_read_published_campaigns_ebooks.sql20260504120000_fix_ebook_covers_mime_types.sql20260504190000_claim_download_resolution.sql
-
Configure Edge Function secrets: provider API key/sender values,
PUBLIC_SITE_URL,SUPABASE_SERVICE_ROLE_KEY -
Keep email sender/domain settings aligned with your current transactional email provider
- 🔁 Replace/overwrite ebook functionality (currently uploads latest file and reads latest attached ebook)
- 🚦 Author-facing draft / publish controls (campaigns default to published today; public read policy exists in migrations)
- 📤 CSV export for entries
- ✍️ Persist design-form edits directly to DB (design page currently drives preview via in-memory
savedLandinginCampaignDesignPage) - 🎨 Continue polish for public giveaway and download page variants
- 🧾 Add richer confirmation messaging around email send status on submit
This project has moved beyond scaffolding into a real, working SaaS foundation. The core backend complexity—auth, storage, security, and data relationships—is now in place.
Future work is primarily:
- UX polish
- conversion optimization
- deliverability tuning (templates, bounce handling, sender authentication)
Status: 🟢 MVP Functional (core auth/campaign/entry/download/email flow live) Next Focus: Publishing workflow hardening + export/reporting + UX polish


