A self-hosted, invite-only book review platform for collecting feedback from beta readers.
- Invite-only access - Control who can read your book via email allowlist
- Magic link authentication - No passwords to manage
- Inline comments - Readers highlight text and leave contextual feedback
- Threaded replies - Respond to comments with nested threads
- Resolve comments - Mark feedback as addressed
- Reading progress tracking - Track scroll position, time spent, and completion
- Reading sessions - Analytics on when and how readers engage
- Admin dashboard - Manage readers, review comments, view analytics
- Multi-book support - Host multiple books on one platform
- Auto-sync from manifests - Books auto-discovered from
public/books/*/manifest.json
- Frontend: Next.js 14 (App Router), React, Tailwind CSS
- Backend: Supabase (Auth, Database, RLS)
- Hosting: Vercel
- Go to supabase.com and create a new project
- Go to Authentication → URL Configuration and add your site URL
- Copy your project URL and anon key from Settings → API
# Link to your project (via npx, no global install needed)
npx supabase link --project-ref YOUR_PROJECT_REF
# Apply migrations
npx supabase db pushcp .env.example .env.localEdit .env.local with your Supabase credentials:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
NEXT_PUBLIC_APP_URL=http://localhost:3000
npm install
npm run devOpen http://localhost:3000.
Global admins can access all books on the platform:
insert into global_admins (user_email)
values ('your-email@example.com');Create a book directory with a manifest:
public/books/my-book/
├── manifest.json
├── 01-introduction.html
├── 02-chapter-two.html
└── ...
Example manifest.json:
{
"title": "My Book Title",
"description": "A description of my book",
"version": "v0.1",
"versionName": "Draft 1",
"chapters": [
{ "slug": "01-introduction", "number": 1, "title": "Introduction", "status": "ready" },
{ "slug": "02-chapter-two", "number": 2, "title": "Chapter Two", "status": "ready" }
]
}Books and chapters auto-sync to the database on deploy (via scripts/sync-books.mjs).
Via Admin Dashboard (recommended):
- Go to
/admin/readers - Enter the reader's email and select a book
- Click "Invite"
Via SQL:
insert into book_access (book_id, user_email)
select id, 'reader@example.com'
from books where slug = 'my-book';When the reader visits the site and enters their email, they'll receive a magic link.
Instead of committing book content directly, you can fetch books from GitHub releases at build time.
Create books.config.json (or copy from books.config.example.json):
{
"books": [
{
"slug": "my-book",
"source": {
"type": "github-release",
"repo": "your-username/your-book-repo",
"release": "latest"
}
}
]
}# Fetch all configured books
npm run fetch-books
# Fetch a specific book
npm run fetch-books -- --book=my-bookFor private repositories, set GITHUB_TOKEN environment variable.
For CI/CD, you can pass the config as an environment variable instead of a file:
BOOKS_CONFIG='{"books":[...]}' npm run fetch-booksThis is useful for Vercel deployments where you don't want to commit your book configuration.
Migrations are stored in supabase/migrations/ and auto-deployed via Vercel.
# Make changes in Supabase Studio, then generate migration:
npx supabase db diff -f my_change_name
# Or create manually:
npx supabase migration new my_change_name
# Then edit supabase/migrations/<timestamp>_my_change_name.sqlAll CI/CD runs through Vercel (no GitHub Actions). The build script (scripts/ci-build.sh) runs:
- Lint
- Build (Next.js)
- Migrate (Supabase)
- Sync books/chapters from manifests
Add these environment variables in Vercel (Settings → Environment Variables):
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Supabase anon key |
NEXT_PUBLIC_APP_URL |
Production URL |
SUPABASE_ACCESS_TOKEN |
For CLI migrations |
SUPABASE_PROJECT_REF |
For CLI migrations |
SUPABASE_SERVICE_ROLE_KEY |
For sync script (bypasses RLS) |
npm run dev # Start development server
npm run build # Build for production
npm run start # Start production server
npm run lint # Run linter
npm run test # Run unit tests
npm run test:watch # Run unit tests in watch mode
npm run test:e2e # Run E2E tests (requires running app)Unit tests (Vitest) run without external dependencies and are included in CI.
E2E tests (Playwright) require a running app with Supabase:
# Local: Start dev server first
npm run dev
npm run test:e2e
# Against deployed URL
PLAYWRIGHT_BASE_URL=https://your-app.vercel.app npm run test:e2esrc/
├── app/
│ ├── login/ # Magic link login
│ ├── auth/ # Auth callbacks (callback, signout, verify)
│ ├── [book]/ # Book table of contents
│ ├── [book]/[chapter]/ # Chapter reader
│ └── admin/ # Admin dashboard
│ ├── readers/ # Manage readers, invite new ones
│ ├── comments/ # Review all comments with filters
│ └── analytics/ # Per-chapter engagement metrics
├── components/
│ └── reader/ # Reader components (ChapterContent, etc.)
├── lib/
│ └── supabase/ # Supabase clients (server, client, middleware)
└── middleware.ts # Auth session refresh
scripts/
├── ci-build.sh # Vercel build script (lint, build, migrate, sync)
└── sync-books.mjs # Sync manifests to database
supabase/
└── migrations/ # Database migrations (auto-deployed)
public/
└── books/ # Book content (HTML chapters + manifest.json)
Licensed under Apache 2.0 with Commons Clause. See LICENSE for details.
This means you can freely use, modify, and distribute this software for non-commercial purposes. Commercial use requires explicit permission from the author.