Skip to content

About

Self-hosted anime tracking and library management with two-way AniList and MyAnimeList sync. Bun/Turborepo monorepo built on Effect, TanStack Start, and Drizzle.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

AnimeKaiser

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.

What it does

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.

Architecture

Bun workspaces, orchestrated by Turborepo: two apps, six packages.

Applications

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.

Packages

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.

Request path

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.

Tech stack

  • Runtime / tooling: Bun 1.4.3, Turborepo, TypeScript 7, Biome
  • Backend: Effect 3, @effect/platform HTTP, @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 test for unit tests, Playwright for end-to-end

Local setup

Prerequisites

  • Bun 1.4.3 or newer
  • Node.js 20 or newer
  • Docker, or a local Postgres 17 and Redis 8

Steps

bun install
cp .env.example .env

Start Postgres and Redis. The repository ships a compose file for both:

bun run --cwd packages/db dev     # docker compose up: postgres:17 + redis:8

Apply migrations, then start everything:

bun run --cwd packages/db db:migrate
bun dev

The web app serves on http://localhost:3000, the API on http://localhost:8080.

Environment variables

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.

Commands

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 tests

Database:

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 Studio

Tests

Unit tests run under Bun's test runner and need no external services:

bun test

End-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:e2e

KAISER_E2E_PASSWORD overrides the throwaway account password used by the onboarding spec.

Screenshots

Home — continue watching, seasonal airing, and discovery rows.

Home

Profile — library breakdown, score distribution, and an activity heatmap derived from watch history.

Profile

My list — the tracked library with status filters, search, and per-title progress. Import and sync activity are driven from here.

My list

Series detail — metadata, library state, and tabs for episodes, relations, and recommendations.

Series detail

License

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.

About

Self-hosted anime tracking and library management with two-way AniList and MyAnimeList sync. Bun/Turborepo monorepo built on Effect, TanStack Start, and Drizzle.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages