A full-stack EPUB reader and spaced repetition vocabulary learning platform.
English | 中文文档
| Desktop | Mobile |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Try it online: https://english-read.bitbw.top/
English Read is a Next.js 15 full-stack web application combining an online EPUB reader with an SRS (Spaced Repetition System) vocabulary learning workflow. Upload EPUBs or browse a shared public library, look up words while reading, and review them with an Ebbinghaus-curve-based flashcard system.
- EPUB Reader — paginated reading, font size control, reading progress auto-saved
- Personal Library & Public Library — upload to your shelf; browse and add books from a shared public catalog
- Vocabulary — collect unknown words from the reader; optional review plan view
- Spaced Repetition Review — Ebbinghaus intervals: 1d → 2d → 4d → 7d → 15d → 30d → mastered; phrase (multi-word) review distractors can use Vercel AI Gateway when configured; single-word paths may not need it
- Dictionary & Translation — English definitions (Free Dictionary API) + Chinese translation (MyMemory); optional Google Cloud Translation fallback; cached 24 h
- Authentication — GitHub and Google OAuth; email/password; phone OTP (Aliyun SMS) via NextAuth v5 with JWT sessions
- Internationalization — English / Chinese UI (next-intl, cookie-based, URL unchanged)
- Dark Mode — system-aware theme toggle
- Observability — Sentry error monitoring; optional PostHog and Vercel Analytics
| Layer | Technology |
|---|---|
| Framework | Next.js 15 + React 19 (App Router) |
| Language | TypeScript (strict) |
| UI | shadcn/ui v4 (@base-ui/react), Tailwind CSS v4 |
| Auth | NextAuth v5 (OAuth + Credentials + phone OTP), JWT sessions |
| Database | Drizzle ORM + Neon serverless PostgreSQL |
| File Storage | Vercel Blob |
| i18n | next-intl ^4.9.1 |
| EPUB Engine | epubjs |
| Errors | @sentry/nextjs |
| Analytics | Optional: PostHog (posthog-js), @vercel/analytics |
- Node.js 18+ (Node 20 LTS recommended for local dev and production)
- npm — this repo ships
package-lock.json; npm is the documented package manager (yarn/pnpm work only if you align lockfiles yourself)
git clone <repo-url>
cd english-read
npm installCreate .env.local in the project root. Minimum for local auth + DB + uploads:
AUTH_SECRET=
AUTH_URL=http://localhost:3000
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
POSTGRES_URL= # Neon pooled connection
POSTGRES_URL_NON_POOLING= # Neon direct connection (used by drizzle-kit)
BLOB_READ_WRITE_TOKEN= # Vercel BlobGenerate AUTH_SECRET with npx auth secret.
Optional integrations (SMS login, AI Gateway for phrase review distractors, translation fallback, analytics) are documented in .env.example.
npx drizzle-kit generate # Generate migration files from schema
npm run db:migrate # Apply migrations (alias for drizzle-kit migrate)
# or: npx drizzle-kit migrate
npx drizzle-kit studio # Open Drizzle Studio (DB browser)npm run dev # Dev server at http://localhost:3000
npm run build # Production build (includes ESLint)
npm run start # Start production server after build
npm run lint # ESLint only
npx tsc --noEmit # TypeScript check onlysrc/
├── app/
│ ├── (app)/ # Authenticated shell (Sidebar + Topbar)
│ │ ├── dashboard/
│ │ ├── library/ # Personal books + upload
│ │ ├── library/store/ # Public library browse & contribute
│ │ ├── vocabulary/ # Word list
│ │ ├── vocabulary/review/ # SRS flashcards
│ │ ├── vocabulary/plan/ # Review planning
│ │ ├── read/[bookId]/ # Full-screen EPUB reader
│ │ └── settings/
│ ├── (auth)/ # login, signup, error (no app shell)
│ ├── api/
│ └── dev/ # Internal/dev-only pages (optional)
├── components/
│ ├── reader/
│ └── ui/
├── lib/
│ ├── db/ # Drizzle schema, migrations, db client
│ ├── srs.ts # Spaced repetition intervals
│ ├── blob.ts # Vercel Blob uploads
│ ├── auth.ts # NextAuth config
│ ├── review-quiz.ts # Review / distractor logic (example)
│ └── … # reading-time, phone-auth, dictionary helpers, etc.
├── i18n/
└── middleware.ts
messages/
├── en.json
└── zh.json
The src/lib/ listing above is not exhaustive — browse the folder for the full set of helpers (e.g. reading-time.ts, phone-auth.ts, aliyun-dypns.ts).
middleware.ts protects /dashboard, /library, /read, /vocabulary, and /settings. The app session is JWT (session.strategy: "jwt"); the sessions table remains for the Drizzle adapter / OAuth linking, not as the primary per-request session store. Providers include GitHub, Google, email/password (hashed with bcrypt), and phone OTP (requires Aliyun env vars). Logged-in users hitting /login or /signup are redirected to /dashboard.
EpubReader is dynamically imported with { ssr: false }. Paginated flow with pixel dimensions from getBoundingClientRect(); touch swipe is registered on each iframe view.window for multi-iframe layouts.
Stages: 0→1d, 1→2d, 2→4d, 3→7d, 4→15d, 5→30d, 6+→mastered. “Forgot” resets to stage 0. The review queue uses vocabulary.nextReviewAt ≤ now.
shadcn/ui v4 on @base-ui/react (not Radix):
- No
asChild— userender={<Link href="..." />} - In Server Components, import
buttonVariantsfrom@/components/ui/button-variants
Hosting target is Vercel. Recommended flow: create Storage → register OAuth apps → copy env into .env.local → run npm run db:migrate once against Neon → connect the GitHub repo and deploy. Migration SQL lives in src/lib/db/migrations/ (drizzle.config.ts uses POSTGRES_URL_NON_POOLING). Optional env keys are listed in .env.example.
Open the Vercel Dashboard → select or create your project → Storage, then provision:
Postgres (Neon)
| Field | Value |
|---|---|
| Type | Postgres |
| Purpose | App data: users, books, vocabulary, reviews, etc. |
| Auto-injected env | POSTGRES_URL, POSTGRES_URL_NON_POOLING |
Link the database to your Vercel project so these variables appear under Settings → Environment Variables (or copy them from the Storage UI for local .env.local).
Blob
| Field | Value |
|---|---|
| Type | Blob |
| Suggested name | english-read-epub |
| Purpose | Uploaded EPUB files |
| Auto-injected env | BLOB_READ_WRITE_TOKEN |
-
Open Google Cloud Console.
-
APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client IDs.
-
Application type: Web application.
-
Under Authorized redirect URIs, add both local and production callbacks:
http://localhost:3000/api/auth/callback/google https://your-domain.vercel.app/api/auth/callback/google -
Save and copy Client ID and Client Secret.
GitHub
- GitHub → Settings → Developer settings → OAuth Apps → New OAuth App.
- Local development:
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
- Homepage URL:
- Production: use a separate OAuth app or change the callback to
https://your-domain.vercel.app/api/auth/callback/github. - Copy Client ID and generate a Client Secret.
In the project root, create .env.local (same keys as Vercel Production unless you use Preview-specific values):
# Auth.js
AUTH_SECRET= # run: npx auth secret
AUTH_URL=http://localhost:3000
# Google OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# GitHub OAuth
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# Neon — from Vercel Storage / project env
POSTGRES_URL=
POSTGRES_URL_NON_POOLING=
# Vercel Blob
BLOB_READ_WRITE_TOKEN=Paste the output of npx auth secret into AUTH_SECRET.
drizzle.config.ts reads .env.local (via @next/env) and applies migrations from src/lib/db/migrations/ using POSTGRES_URL_NON_POOLING.
-
First deploy / empty database: with
POSTGRES_URL_NON_POOLINGset (local file pointing at your Neon DB), run:npm run db:migrate
Do not run
npx drizzle-kit generateon the server unless you are changing the schema—committed migration files are the source of truth. -
When you change
src/lib/db/schema.tslocally: runnpx drizzle-kit generate, commit the new files undersrc/lib/db/migrations/, deploy, then runnpm run db:migrateagainst each environment that should pick up the change.
Optional: npx drizzle-kit studio opens Drizzle Studio against the same connection.
npm run devOpen http://localhost:3000.
Option A — Git integration (recommended)
Import the GitHub repository in Vercel, select the framework (Next.js), add Environment Variables for Production (and Preview if needed), then push to main (or your production branch). Build command is npm run build by default.
Option B — Vercel CLI
npm i -g vercel # if needed
vercel # preview
vercel --prod # production-
Environment variables — Mirror
.env.localin Vercel → Settings → Environment Variables. SetAUTH_URLto your production site origin (e.g.https://your-domain.vercel.app), nothttp://localhost:3000. -
OAuth — Add production redirect URIs in Google Cloud Console and GitHub OAuth App settings (see §2).
-
Optional product features — Names and comments match
.env.example. Where to obtain:Variables Purpose Where to obtain AI_GATEWAY_API_KEYPhrase (multi-word) review distractors via GET /api/review/similar-words(required for that branch; single-word review may use other sources)Vercel Dashboard → AI → AI Gateway → API keys · Vercel AI Gateway GOOGLE_TRANSLATE_API_KEYMachine translation fallback for /api/dictionaryGoogle Cloud Console → enable Cloud Translation API → APIs & Services → Credentials → Create API key NEXT_PUBLIC_POSTHOG_KEY,NEXT_PUBLIC_POSTHOG_HOSTPostHog client analytics PostHog → Project settings → Project API Key; host = your region’s ingestion URL (regions, e.g. https://us.i.posthog.com)ALIBABA_CLOUD_ACCESS_KEY_ID,ALIBABA_CLOUD_ACCESS_KEY_SECRET,ALIYUN_SMS_SIGN_NAME,ALIYUN_SMS_TEMPLATE_CODE, …Aliyun SMS OTP (DYPNS) login AccessKey: RAM; SMS / phone verification: 号码认证控制台 (sign & template); RAM needs dypns:SendSmsVerifyCode/CheckSmsVerifyCode; notes indocs/阿里云/SENTRY_AUTH_TOKENUpload source maps during next build(clearer stacks)sentry.io → your organization → Settings → Developer Settings → Auth Tokens · Next.js source maps Sentry runtime — DSN is embedded; errors are reported when
NODE_ENV === "production".SENTRY_AUTH_TOKENis only required if you want source maps uploaded on Vercel builds.
| Table | Purpose |
|---|---|
users |
User profiles (Auth.js); email, phone, password hash |
accounts |
OAuth provider links (Auth.js) |
sessions |
Auth.js Drizzle adapter table; app uses JWT for the live session, not DB-backed sessions per request |
verification_tokens |
Email verification tokens (Auth.js) |
public_library_books |
Shared catalog entries |
books |
Personal shelf; EPUB blob URL and reading progress |
reading_daily_time |
Per-day reading time (seconds) |
vocabulary |
Saved words and SRS schedule |
review_logs |
Each review outcome (remembered / forgotten) |













