AnimeKaiser is a self-hosted anime tracker. You get a personal library with watch status and episode progress, a discovery catalogue pulled from public anime metadata, and two-way sync with AniList and MyAnimeList. A separate streaming service resolves playback sources, and whatever you watch gets recorded back to your library.
Library management. Every entry is keyed by MyAnimeList ID and tracks status, episode count, score, and privacy. Libraries can be public or private, with profile pages and stats for the public ones.
Syncing with AniList/MAL. Link an account over OAuth and AnimeKaiser
imports your existing list once, then pushes local changes back out from then
on. Sync happens through an event log (library_sync_event rows worked by a
background job), so a failed sync is visible and retryable instead of quietly
blocking a write. Refresh tokens get rotated by their own worker.
Discovery. Browse, search, a weekly airing schedule, a latest-episodes feed, and a "surprise me" endpoint. It's backed by AniList's GraphQL API and Jikan (the MAL REST mirror), with Redis caching in front of both. ani.zip adds per-episode titles, synopses, stills and air dates, plus series logo art for the home hero.
Airing tracking. An hourly worker asks AniList for the airing status of
every show someone is watching or planning, then checks the providers to see
whether the new episode is actually streamable. The worker is backed by the
anime_airing_state table. A show you add to your list starts being tracked
right away. That drives the "up next" cards in continue watching and the
plan-to-watch row for airing shows.
Streaming. Finding and proxying actual streams is handled by a separate
service that isn't part of this repo. AnimeKaiser only talks to it over a
typed RPC contract (StreamingClient in apps/api/src/infra/streaming.ts)
and only ever stores the anime-to-provider mappings it hands back — never the
stream data itself, and no provider is hardcoded. Bring your own
implementation and point STREAMING_SERVICE_URL / STREAMING_SECRET at it.
Both are required; the API won't start without them. If the service is
down or unreachable, streaming shows as unavailable and nothing else breaks.
The web app has a player with subtitles, server and audio selection, seek thumbnails, skip intro/outro, and a theater mode that keeps the episode list beside the video. Watch progress feeds a history grouped by show and the "continue watching" row. Playback settings cover default audio, preferred provider, subtitle language, spoiler blur for unwatched episodes, and landscape-on-fullscreen for phones. ⌘K opens a command palette for search, pages and quick actions like switching between English and romaji titles.
Bun workspaces, orchestrated by Turborepo: two apps, six packages.
| Path | Role |
|---|---|
apps/api |
Backend. Composes the Effect layer graph: Postgres, Redis, Better Auth, R2 media storage, HTTP routes, the RPC server, and three background workers (library import, library sync, token refresh). Entry point is src/app.ts. |
apps/web |
TanStack Start browser application. File-based routes in src/routes, feature modules in src/features, state via effect-atom. Talks to the API over the RPC WebSocket. |
| Path | Role |
|---|---|
packages/domain |
Infrastructure-free effect/Schema models and @effect/rpc contracts. The single shared contract between server and client. KaiserRpcs in src/rpc.ts merges every RPC group. |
packages/db |
Drizzle schema, migrations, and the Database service. Schema is split by concern: auth, profile, library, external-list-account, history, streaming. |
packages/core |
Backend business services — anime metadata, profiles, library import/sync, external-list OAuth, watch history, stream resolution. Depends on db and domain, never on HTTP composition. |
packages/rpc |
Effect RPC server: handler layers per domain group, plus authentication middleware and request logging. RpcLive mounts the group at /rpc over WebSocket with NDJSON serialisation. |
packages/auth |
Better Auth server and web client configuration. Auth tables live in packages/db. |
packages/ui |
Shared React components, hooks, and global styles, exported through components/* entry points. |
The web app calls a typed RPC method from packages/domain. That request hits
packages/rpc's handler layer, which resolves a service from packages/core,
which reads or writes through packages/db. The only things living directly
in apps/api/src/routes are HTTP concerns like CORS and auth callbacks.
No separate queue for background work — it's dispatched through Postgres
LISTEN/NOTIFY, with DatabaseListenerLive in
apps/api/src/infra/database.ts waking the workers up.
- Runtime / tooling: Bun 1.4.3, Turborepo, TypeScript 7, Biome
- Backend: Effect 3,
@effect/platformHTTP,@effect/rpc, Drizzle ORM, Postgres 17, Redis 8 - Auth: Better Auth (email/password, passkeys, OAuth account linking)
- Frontend: TanStack Start, React 19, TanStack Router,
effect-atom, Tailwind - External services: AniList GraphQL, Jikan, Resend (transactional email), Cloudflare R2 (profile media)
- Testing:
bun testfor unit tests, Playwright for end-to-end
- Bun 1.4.3 or newer
- Node.js 20 or newer
- Docker, or a local Postgres 17 and Redis 8
bun install
cp .env.example .envStart Postgres and Redis. The repository ships a compose file for both:
bun run --cwd packages/db dev # docker compose up: postgres:17 + redis:8Apply migrations, then start everything:
bun run --cwd packages/db db:migrate
bun devThe web app serves on http://localhost:3000, the API on http://localhost:8080.
All read in apps/api/src/env.ts. Anything without a default is required —
the API won't start without it. See .env.example for the full list.
| Variable | Notes |
|---|---|
DATABASE_URL, REDIS_URL |
Match the compose file defaults out of the box. |
PORT, ENV |
Default to 8080 and dev. |
APP_URL, VITE_API_URL, BETTER_AUTH_URL |
Origins for CORS, the client, and auth callbacks. |
BETTER_AUTH_SECRET |
Any high-entropy string. Change it outside development. |
AUTH_COOKIE_DOMAIN |
Leave empty on localhost. |
MAL_CLIENT_ID / MAL_CLIENT_SECRET |
MyAnimeList OAuth app. Required for MAL sync. |
ANILIST_CLIENT_ID / ANILIST_CLIENT_SECRET |
AniList OAuth app. Required for AniList sync. |
RESEND_API_KEY, AUTH_EMAIL_FROM |
Verification and password-reset email. |
R2_* |
S3-compatible bucket for profile media. Any S3-compatible endpoint works. |
DISCORD_ALERT_WEBHOOK_URL |
Optional. Fatal startup/runtime crashes post here. |
bun dev # All development tasks
bun build # Build all workspaces
bun typecheck # Type-check all workspaces
bun lint # Biome check
bun format # Biome format --write
bun test # Unit tests
bun test:e2e # Playwright end-to-end testsDatabase:
bun run --cwd packages/db dev # Start Postgres + Redis via Docker
bun run --cwd packages/db db:generate # Generate a migration from schema changes
bun run --cwd packages/db db:migrate # Apply migrations
bun run --cwd packages/db db:studio # Drizzle StudioUnit tests run under Bun's test runner and need no external services:
bun testEnd-to-end tests use Playwright and require a running API, web app, Postgres, and Redis. Install the browser once, then run:
bun test:e2e:install
bun test:e2eKAISER_E2E_PASSWORD overrides the throwaway account password used by the
onboarding spec.
Home — continue watching, seasonal airing, and discovery rows.
Profile — library breakdown, score distribution, and an activity heatmap derived from watch history.
My list — the tracked library with status filters, search, and per-title progress. Import and sync activity are driven from here.
Series detail — metadata, library state, and tabs for episodes, relations, and recommendations.
GNU Affero General Public License v3.0.
If you run a modified version of this software as a network service, the AGPL requires you to make its source available to that service's users.



