animetana.com
2026-03-20Building an Anime Platform from Japan
I live in Japan. Anime, manga, light novels, visual novels — this stuff isn't a hobby I picked up from the internet. It's the culture I'm surrounded by every day. Bookstores with entire floors dedicated to manga. Convenience stores selling light novel volumes. Akihabara twenty minutes away. After years of using various anime tracking sites and finding them either bloated, slow, or built by people who clearly don't care about the medium the way I do, I decided to build my own.
animetana.com is the result. An anime, manga, light novel, and visual novel platform. What follows is the technical story of how it's built, what I chose, and why.
The Monorepo
The project is a pnpm workspace monorepo. Multiple services live in apps/ and shared code lives in packages/. The workspace structure:
apps/
api/ — Hono REST API with Drizzle ORM
web/ — Astro frontend with React islands
workers/ — Node.js background jobs on cron schedules
packages/
shared/ — Database schema, TypeScript types
Every service imports from @animetana/shared. The database schema is defined once and used everywhere — API route handlers, worker jobs, type generation. When I add a column to a table, the TypeScript compiler catches every place that needs updating across all services. No drift between services, no out-of-date types.
I considered splitting into separate repos early on. Glad I didn't. A monorepo means one git pull, one dependency tree, one CI pipeline. The overhead of coordinating changes across multiple repos for a single-person project would be pure waste.
Frontend — Astro + React
The frontend runs on Astro with React components. Astro's island architecture is the key decision here. Most pages on animetana.com are content-heavy — anime details, character lists, rankings. These pages are mostly static HTML. They don't need a JavaScript runtime to render a synopsis or display a cover image.
React only hydrates where interactivity is genuinely needed: search components, tracking buttons, filter panels, user menus. Everything else ships as zero-JS HTML. The result is fast initial page loads with interactive elements that feel responsive.
I use Astro's file-based routing. Each page is an .astro file that fetches data at build time or request time, renders the shell, and slots in React components where needed. The mental model is simple: Astro handles the page, React handles the widgets.
Styling is a single global CSS file. No CSS-in-JS, no utility framework. The site has a cohesive visual identity and I want full control over it. One stylesheet means I can see every style rule in one place, refactor confidently, and avoid specificity conflicts that come from scattered component-level styles.
API — Hono + Drizzle ORM
The API is built on Hono. If you haven't used it, Hono is a lightweight TypeScript web framework that runs on any JavaScript runtime. It's fast, has excellent TypeScript inference, and the API surface is minimal. Coming from Rust and C, I have zero patience for frameworks that hide control flow behind decorators and magic. Hono gives you a router, middleware support, and gets out of the way.
Drizzle ORM handles all database access. It generates TypeScript types directly from the schema definition, so queries are fully typed end to end. When I write a select statement, the return type matches the actual columns. When I write an insert, the compiler enforces required fields. No any types leaking through the data layer.
The schema lives in packages/shared/src/db/schema/ and is shared across the API and workers. Both services import the same table definitions, so there's no possibility of the API assuming one column type while the workers assume another.
Route handlers are organized by domain: media, tracking, auth, users. Each file exports a Hono app that gets mounted on the main router. The structure is flat and predictable. Finding where an endpoint is defined takes seconds.
Database — PostgreSQL
PostgreSQL is the only database. No secondary stores, no document databases, no graph databases bolted on for specific features. Postgres handles everything: anime metadata, user accounts, tracking lists, tags, relationships between entities.
The schema uses Drizzle migrations. pnpm db:generate creates migration files from schema changes, pnpm db:migrate applies them. Drizzle Studio provides a GUI for inspecting data during development. The migration workflow is straightforward — change the schema, generate, migrate, done.
All data ingestion jobs use upserts with ON CONFLICT DO UPDATE. This makes every job idempotent. Running the same job twice produces the same result. No duplicate rows, no constraint violations.
There's a pattern throughout the codebase for protecting AI-enriched content. Several tables have an enriched boolean column. When a job upserts a row, it uses a CASE WHEN expression: if the row is already enriched, preserve the existing translated content; otherwise, overwrite with the new data. This prevents ingestion from clobbering expensive AI translations with raw text.
Cache — Redis
Redis sits in front of PostgreSQL for read-heavy endpoints. Anime detail pages, rankings, popular series — these get hit constantly and the underlying data changes infrequently. Caching these responses in Redis eliminates redundant database queries and keeps response times low.
The cache invalidation strategy is simple: the worker jobs that update the data also clear the relevant cache keys. Since the workers and the API share the same Redis instance and the same key conventions via the shared package, there's no coordination problem. Data updates, cache clears, next request rebuilds the cache. No TTL guessing, no stale data.
Storage — AWS S3
All images — cover art, character portraits, banners — live in S3. The API uploads processed images to the bucket, and they're served through a CDN at cdn.animetana.com. Keeping media out of the database and off the application server is basic hygiene, but it matters especially here because animetana.com is image-heavy. Every anime entry has a cover. Many have multiple character images. Serving these from S3 through a CDN means the origin server never touches image bytes during normal operation.
For local development, MinIO provides S3-compatible storage so the full upload/serve pipeline works identically on my machine without touching production buckets.
Background Workers
The workers service is where the real complexity lives. Multiple parallel pipelines run on cron schedules, handling data ingestion, image processing, and AI enrichment.
The enrichment pipeline translates English synopses to Japanese using AI. This is the most expensive operation in the system — both in time and cost. The enriched boolean flag in the database ensures each record is only translated once. Re-running the pipeline skips already-enriched rows.
Workers run as a separate Docker container. They share the database and Redis connections with the API but have their own process and their own cron schedule. If a worker job crashes or runs long, it doesn't affect API response times. The separation is simple but important for reliability.
Infrastructure — Docker + Nginx + Cloudflare
Production runs on an EC2 instance in ap-northeast-1 (Tokyo). Everything is containerized with Docker Compose. PostgreSQL, Redis, the API, the web frontend, and workers each run in their own container on a shared Docker network.
Nginx runs on the host as a reverse proxy. animetana.com routes to the web container, api.animetana.com routes to the API container. SSL terminates at Cloudflare with a self-signed origin certificate on the server. Cloudflare handles DNS, CDN caching for static assets, and DDoS protection.
The deploy process is deliberately simple. Push to main, SSH into the server, pull, rebuild the affected containers, restart. No CI/CD pipeline, no Kubernetes, no orchestrator. For a single-person project, the complexity of automated deployment infrastructure would exceed the complexity of the actual deployment. docker compose build && docker compose up -d takes under a minute and I can see exactly what's happening.
Database backups run every 12 hours via cron, dumping to S3. Recovery is a single gunzip | psql command. Simple, tested, reliable.
Why This Stack
Every decision comes back to two things: keeping the system understandable and keeping it fast.
Understandable means I can hold the entire architecture in my head. One repo, one database, one cache, one deployment target. No microservice boundaries to reason about, no eventual consistency to debug, no message queues to monitor. When something breaks at 2am, I need to find the problem in minutes, not hours.
Fast means fast for users. Astro ships minimal JavaScript. Redis eliminates redundant queries. S3 and Cloudflare CDN serve images from the edge. The Tokyo region placement means domestic users — the primary audience — get single-digit millisecond latency to the origin.
I write Rust and C for systems work. That background gives me low tolerance for unnecessary abstraction. This stack is typed end to end, deploys in under a minute, and serves pages in milliseconds. It does what I need and nothing more.
If you're into anime, manga, light novels, or visual novels — check out animetana.com.