One account to track everything you consume in Japanese — anime, manga, visual novels, light novels, books, movies, series, YouTube, podcasts, drama CDs, games — with the language-learning metrics layered on top: hours, characters, streaks, levels, goals, rankings and clubs.
Think Toggl for immersion, with a library attached.
/log/new): pick a medium → search your library and AniList / VNDB / TMDB / Google Books → fill in the details. New titles are added to your library automatically, with covers, Japanese titles and known lengths.ws://localhost:2333/api/ws/text/origin) or Textractor (ws://localhost:6677) from the browser; lines stream in, characters and active time (idle gaps excluded) are counted, and one click saves the session against your VN.Five destinations — Home, Library, Discover, Community, Stats — plus one Log button. Everything else (session log, goals, texthooker, settings, your profile) lives in the account menu. On phones the same five become a bottom tab bar with logging in the middle.
Signed-out visitors get a public landing page at / with live community numbers; everything else redirects to /login.
Next.js 16 (App Router, server actions) · TypeScript · Tailwind v4 + shadcn/ui (Base UI) · Drizzle ORM · Postgres (PGlite embedded for local dev) · Better Auth (email + password).
pnpm install
cp .env.example .env.local # then set BETTER_AUTH_SECRET (see the file)
pnpm db:push # creates ./.pglite with the schema
pnpm dev
Open http://localhost:3000, create an account, add something to your library, start the timer.
The community pages are dull with one account, so there is a seeder:
pnpm seed:demo # ~9 demo members, real covers from AniList/VNDB, months of sessions, follows, kudos, a club
pnpm seed:demo --reset # delete previously seeded demo members first
Demo accounts are real accounts you can sign in as: <handle>@demo.immersionlog.com / immerse-demo-2026 (handles are printed at the end of the run). They are recognisable by that email domain, which is also how --reset finds them — never run the seeder against a production database.
| Variable | Required | Notes |
|---|---|---|
BETTER_AUTH_SECRET | yes | node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" |
BETTER_AUTH_URL | yes | http://localhost:3000 locally; your public URL in production |
DATABASE_URL | production | Postgres connection string. Unset = embedded PGlite in ./.pglite |
TMDB_API_KEY | optional | Enables movie/series search. Free at themoviedb.org → Settings → API |
GOOGLE_BOOKS_API_KEY | optional | Books search works without it (rate-limited) |
AniList and VNDB need no keys.
Without DATABASE_URL the app uses an embedded Postgres in ./.pglite. It is single-process: stop pnpm dev before running pnpm db:push or pnpm build, otherwise the data directory can be corrupted (delete ./.pglite and push again if that happens).
| Script | |
|---|---|
pnpm dev | dev server |
pnpm build / pnpm start | production |
pnpm db:push | apply the Drizzle schema (dev) |
pnpm db:generate / pnpm db:migrate | SQL migrations for production |
pnpm db:studio | Drizzle Studio |
pnpm seed:demo | demo community (see above) |
pnpm lint / pnpm typecheck |
media_items shared across users; deduplicated on (source, source_id); cover + banner art
library_entries user × item: status, progress (native unit), rating, notes
immersion_sessions the core primitive: started_at, duration, optional item, optional amount + unit
active_timers one running timer per user
goals metric (time | unit), optional media type, date range, target
follows directed, no approval; private profiles never appear in feeds or rankings
session_kudos one heart per (session, user)
clubs / club_members / club_picks / club_pick_votes
Time is the common denominator (all sessions have a duration); native units stay per-medium. Days are bucketed in the user's timezone (captured at signup, editable in settings).
The Texthooker page opens a WebSocket from your browser to your own machine; nothing about your game text touches the server until you press Save session. Each message is treated as one line (plain text, or JSON with a text field). Characters are counted excluding whitespace and punctuation. Gaps between lines longer than the idle threshold (default 3 min) are not counted as reading time.
40 commits
TypeScript
97.0%
CSS
2.9%
One account to track everything you consume in Japanese — anime, manga, visual novels, light novels, books, movies, series, YouTube, podcasts, drama CDs, games — with the language-learning metrics layered on top: hours, characters, streaks, levels, goals, rankings and clubs.
Think Toggl for immersion, with a library attached.
/log/new): pick a medium → search your library and AniList / VNDB / TMDB / Google Books → fill in the details. New titles are added to your library automatically, with covers, Japanese titles and known lengths.ws://localhost:2333/api/ws/text/origin) or Textractor (ws://localhost:6677) from the browser; lines stream in, characters and active time (idle gaps excluded) are counted, and one click saves the session against your VN.Five destinations — Home, Library, Discover, Community, Stats — plus one Log button. Everything else (session log, goals, texthooker, settings, your profile) lives in the account menu. On phones the same five become a bottom tab bar with logging in the middle.
Signed-out visitors get a public landing page at / with live community numbers; everything else redirects to /login.
Next.js 16 (App Router, server actions) · TypeScript · Tailwind v4 + shadcn/ui (Base UI) · Drizzle ORM · Postgres (PGlite embedded for local dev) · Better Auth (email + password).
pnpm install
cp .env.example .env.local # then set BETTER_AUTH_SECRET (see the file)
pnpm db:push # creates ./.pglite with the schema
pnpm dev
Open http://localhost:3000, create an account, add something to your library, start the timer.
The community pages are dull with one account, so there is a seeder:
pnpm seed:demo # ~9 demo members, real covers from AniList/VNDB, months of sessions, follows, kudos, a club
pnpm seed:demo --reset # delete previously seeded demo members first
Demo accounts are real accounts you can sign in as: <handle>@demo.immersionlog.com / immerse-demo-2026 (handles are printed at the end of the run). They are recognisable by that email domain, which is also how --reset finds them — never run the seeder against a production database.
| Variable | Required | Notes |
|---|---|---|
BETTER_AUTH_SECRET | yes | node -e "console.log(require('crypto').randomBytes(32).toString('base64'))" |
BETTER_AUTH_URL | yes | http://localhost:3000 locally; your public URL in production |
DATABASE_URL | production | Postgres connection string. Unset = embedded PGlite in ./.pglite |
TMDB_API_KEY | optional | Enables movie/series search. Free at themoviedb.org → Settings → API |
GOOGLE_BOOKS_API_KEY | optional | Books search works without it (rate-limited) |
AniList and VNDB need no keys.
Without DATABASE_URL the app uses an embedded Postgres in ./.pglite. It is single-process: stop pnpm dev before running pnpm db:push or pnpm build, otherwise the data directory can be corrupted (delete ./.pglite and push again if that happens).
| Script | |
|---|---|
pnpm dev | dev server |
pnpm build / pnpm start | production |
pnpm db:push | apply the Drizzle schema (dev) |
pnpm db:generate / pnpm db:migrate | SQL migrations for production |
pnpm db:studio | Drizzle Studio |
pnpm seed:demo | demo community (see above) |
pnpm lint / pnpm typecheck |
media_items shared across users; deduplicated on (source, source_id); cover + banner art
library_entries user × item: status, progress (native unit), rating, notes
immersion_sessions the core primitive: started_at, duration, optional item, optional amount + unit
active_timers one running timer per user
goals metric (time | unit), optional media type, date range, target
follows directed, no approval; private profiles never appear in feeds or rankings
session_kudos one heart per (session, user)
clubs / club_members / club_picks / club_pick_votes
Time is the common denominator (all sessions have a duration); native units stay per-medium. Days are bucketed in the user's timezone (captured at signup, editable in settings).
The Texthooker page opens a WebSocket from your browser to your own machine; nothing about your game text touches the server until you press Save session. Each message is treated as one line (plain text, or JSON with a text field). Characters are counted excluding whitespace and punctuation. Gaps between lines longer than the idle threshold (default 3 min) are not counted as reading time.
40 commits
TypeScript
97.0%
CSS
2.9%