Denn is a multi-media tracker for movies, TV shows, games, albums, and books. It combines public metadata discovery with authenticated user-owned features such as lists, ratings, shared list membership, and stable content-detail routes backed by local persistence.
The project is built as a single monorepo with three cooperating services:
| Path | Stack | Responsibility |
|---|---|---|
web/ |
TanStack Start (Vite + Nitro) + React 19 + TypeScript | Frontend UI, SSR, protected routes, and the browser-facing BFF for metadata |
core/ |
Django 5.2 + DRF | Auth, lists, ratings, invitations, content persistence, and local-first detail reads |
proxy/ |
Go 1.25 + Gin | External metadata gateway for TMDB, IGDB, Spotify, and OpenLibrary |
At a product level, Denn currently supports:
- Multi-source discovery across movies, TV shows, games, albums, and books on a public homepage.
- Public search and content-detail views routed by internal Denn content id; no account is required to explore.
- User authentication with protected routes.
- First-class personal tracking with backlog, in-progress, completed, on-hold, and dropped states.
- Public user profiles with favorites, reviews, completions, and public lists.
- Personal and shared lists.
- Anonymous reads for public content details and public lists.
- List invitations and membership workflows.
- List-item status workflows, ratings, and canonical list ordering.
- Advanced list exploration with filters, grouping, range filters, and multi-field sort.
- Local-first content detail persistence in
core, so detail payloads can be reconstructed from PostgreSQL instead of always hitting the upstream metadata layer.
The former marketing landing's cover-gallery identity now frames the
login and registration surfaces; /welcome has been retired so the
public catalog at / remains Denn's single entry point.
Public browse/taxonomy, richer public content aggregates, full Lists 2.0
roles, and leaderboards remain planned.
The integration model is intentionally hybrid:
- Browser ->
webBFF ->corefor authenticated domain data, auth lifecycle calls, and safe public content-detail reads. - Browser ->
webBFF ->proxyfor public metadata calls. webserver loaders and server functions ->proxyfor SSR metadata reads.webserver ->corefor trusted bulk resolution of stable content ids in public discovery.core->proxyfor enrichment and refresh of persisted content.
In other words:
webserves pages and mediates browser-safe metadata access.coreowns the product domain and local persistence.proxyis the only service allowed to talk directly to third-party metadata providers.
This boundary is a deliberate architectural decision, not an accident. The rationale and tradeoffs live in .docs/adr/0001-external-metadata-integration.md.
The canonical documentation entrypoint is .docs/README.md.
For most work, read in this order:
.docs/architecture/current-state.md.docs/features/implemented.md.docs/technical-debt.md.docs/roadmap/open-plans.md.docs/contracts/internal-http.md.docs/adr/0001-external-metadata-integration.md.docs/adr/0002-web-auth-cookies.md.docs/adr/0003-migrate-web-from-nextjs-to-tanstack-start.md.docs/adr/0004-public-catalog-auth-boundary.md.docs/adr/0005-unified-personal-progress.md
Operational rule:
architecture/*documents merged behavior.features/implemented.mdis the shipped baseline.roadmap/open-plans.mdis the condensed backlog..docs/sprints/contains only open or future execution plans.history/implementation-history.mdstores outcomes extracted from completed sprint docs.
Practical usage flow:
- If you want to understand the repo:
README.md->.docs/README.md - If you want to know how the system works today:
architecture/current-state.md - If you want to know what already exists:
features/implemented.md - If you want to know what comes next:
roadmap/open-plans.md - If you are about to execute or verify something sensitive:
runbooks/ - If you are changing a structural decision:
adr/ - If you are evaluating an idea that is not approved yet:
ideas/
When resuming the project, do not jump directly into the first sprint file. Read:
README.md.docs/README.mdarchitecture/current-state.mdfeatures/implemented.mdtechnical-debt.mdroadmap/open-plans.md- then the first relevant active sprint under
.docs/sprints/
That keeps implementation work anchored to the real current state, not to stale assumptions inside an older plan.
Denn uses an internal content id as the stable public route key:
- frontend detail route:
/content/<id>(TanStack file routeweb/src/routes/content/$id.tsx) - persistence anchor:
core.content.models.ContentItem - upstream identifiers: kept as provider-specific source fields behind the internal id
core stores typed local detail rows and reconstructs proxy-shaped payloads from them on the read path. Fresh local data is served immediately; stale or missing data is refreshed through proxy.
The auth model is server-mediated:
- JWTs exist only in
HttpOnly, production-Secure,SameSite=Laxcookies - the root route resolves session server-side (
beforeLoad/ server functions) and bootstraps the client store globally - browser API calls use same-origin BFF routes and mutations require a double-submit CSRF token
- Zustand/localStorage contain identity and UI state, never JWTs
That roadmap is documented in .docs/adr/0002-web-auth-cookies.md.
.
├── web/ # TanStack Start (Vite + Nitro) frontend + BFF
├── core/ # Django/DRF domain API
├── proxy/ # Go metadata gateway
├── .docs/ # Architecture, contracts, roadmap, runbooks, history
├── .github/ # CI and deploy workflows
├── .scripts/ # Workspace helpers
└── Makefile # Root entrypoints for local work and validation
The frontend owns UI composition, SSR, route protection, and the browser-facing proxy BFF.
Important directories:
web/src/routes/- TanStack Router file routes (pages +api/*server handlers)web/src/components/,web/src/hooks/,web/src/providers/,web/src/stores/- UI and client stateweb/src/lib/- API clients, TanStack Query keys/hooks, utilitiesweb/src/server/- server-only helpers (createServerFn, proxy BFF helpers, session)
Key implementation traits:
- TanStack Start + TanStack Router + Vite 7; production server is Nitro (
.output/) - id-first content routing
- TanStack Query for server-state; route loaders call
ensureQueryData(no Next.jsHydrationBoundary) - Zustand for auth/session UI state
- Runtime public config for the browser is injected via
window.__ENV__from the server (seeweb/src/server/runtime-env.ts); authenticated core URLs and credentials remain server-only
The backend API owns user-authenticated domain behavior and content storage. It is not a general-purpose metadata gateway.
Important directories:
core/authentication/- login, registration, logout, auth endpointscore/content/- models, services, serializers, views, testscore/core/settings/- modular settings
Key implementation traits:
- Django + Django REST Framework
- JWT auth via
dj-rest-authand SimpleJWT - local-first content detail persistence
- derived browse metadata for list exploration
- shared cross-service error envelope with
proxy
The proxy is a stateless Go service that shields the rest of the system from provider-specific schemas, credentials, rate limits, and caching concerns.
Important directories:
proxy/cmd/api/- entrypoint and wiringproxy/internal/providers/- raw upstream clientsproxy/internal/services/- orchestration and mappingproxy/internal/handlers/- HTTP handlers
Key implementation traits:
- Gin HTTP server
- provider integrations for TMDB, IGDB, Spotify, and OpenLibrary
- Redis-backed cache and rate limiting with graceful degradation
- structured JSON logs and request correlation
- TanStack Start + TanStack Router
- Vite 7 + Nitro (production Node server)
- React 19
- TypeScript 5
- Tailwind CSS 4 (
@tailwindcss/vite) - TanStack Query 5
- Zustand 5
- Radix UI
- React Hook Form + Zod
- DnD Kit
- Vitest + Testing Library (frontend tests)
Use a current Node.js LTS locally (TanStack Start / Vite 7 expect Node 20.19+ or 22.12+ per upstream engine ranges).
- Python 3
- Django 5.2
- Django REST Framework 3.16
dj-rest-auth- SimpleJWT
drf-spectacular- Redis support via
django-redis - PostgreSQL in normal environments, SQLite fallback in limited local setups
- Go 1.25
- Gin
go-redis- Provider-specific adapters and mappers per external source
These are non-negotiable unless the docs and tests are updated in the same change:
proxyis the only owner of provider credentials.PROXY_API_KEYis server-only. Never expose it in client bundles orwindow.__ENV__(do not use a public env prefix for secrets).coreis not a general-purpose metadata gateway.proxymust remain stateless with respect to PostgreSQL and user sessions.X-Request-Idpropagation and the canonical error envelope are shared contracts acrosscoreandproxy.- Content detail navigation should use the internal Denn content id once it exists.
The canonical contract is documented in .docs/contracts/internal-http.md.
The full development stack runs in Docker Compose and publishes only to loopback. Each Git worktree gets its own Compose project, network, PostgreSQL volume, Redis container and host-port block. This permits parallel agent and human work without sharing database state.
From a worktree, the default commands create or reuse that worktree's instance:
make check
make setup-local
make up
make smoke-localThe command prints the assigned URLs. The first instance normally uses
http://127.0.0.1:3000, with Core on 8000 and the proxy on 8080.
Subsequent instances automatically use the next free block, such as
web 3001, Core 8001, proxy 8081, PostgreSQL 5433 and Redis 6391.
The frontend port can be selected explicitly:
Run each command from its corresponding Git worktree:
make local-up INSTANCE=feature-a WEB_PORT=3000
make local-up INSTANCE=feature-b WEB_PORT=3001
make local-up INSTANCE=feature-c WEB_PORT=3002Inside Compose, services continue to communicate through web, core,
proxy, postgres and redis; only loopback host ports vary. Source is
bind-mounted, Vite and Django reload automatically, and Go changes use
make restart-proxy.
Useful instance-scoped commands:
make local-status INSTANCE=feature-b
make local-logs INSTANCE=feature-b SERVICE=web
make local-smoke INSTANCE=feature-b
make local-browser INSTANCE=feature-b
make local-down INSTANCE=feature-b
make local-destroy INSTANCE=feature-blocal-destroy is the only command that removes the instance's
project-scoped Docker volumes. It does not affect another worktree and it
does not touch production images, deployments or volumes.
Create local-only web/.env, core/.env, and proxy/.env files from
their examples. make check rejects production service/database URLs,
secure-cookie production settings, mismatched internal keys, and public
secret prefixes. It never prints secret values.
To share those private files across Git worktrees without putting them in Git:
# Once, from the configured worktree
make env-store
# In every additional worktree
make env-linkThe default private store is ~/.config/denn/local; override it with
DENN_LOCAL_ENV_DIR. The canonical env-var ownership matrix lives in
.docs/contracts/internal-http.md.
PROXY_API_KEY remains server-only.
To populate an isolated PostgreSQL instance from a validated local dump:
make local-up INSTANCE=feature-b WEB_PORT=3001
make local-clone INSTANCE=feature-b FILE=backups/denn-feature-a-db-<timestamp>.sql.gz
make local-smoke INSTANCE=feature-b REQUIRE_LOCAL_SNAPSHOT=trueThe dump is restored into the selected worktree only. Do not connect a local instance directly to production or copy production credentials into the repository.
make browser-local targets the current worktree's running stack and
make local-browser INSTANCE=... makes that scope explicit. The local
browser smoke may use the restored local snapshot. It is deliberately
separate from make e2e-web, whose deterministic, non-personal fixtures
remain the CI gate.
See the local development runbook for snapshot backup/restore, agent usage, security boundaries, and recovery.
Root validation commands:
make validate-web
make validate-core INSTANCE=feature-a
make validate-proxy
make test
make e2e-web
make e2e-web-regressions
make e2e-web-performanceNotes:
make validate-webruns lint plus the production build.make validate-core INSTANCE=<id>targets that instance's running local PostgreSQL; withoutINSTANCE, it preserves the CI-friendly host test behavior and depends on the configured test database.make validate-proxymust remain deterministic and offline-safe.make e2e-webbuilds the Nitro production bundle and runs the stable Playwright smoke on desktop and mobile.make e2e-web-regressionsreproduces quarantined audit failures;make e2e-web-performancewrites the repeatable cold/warm baseline.
The shared error-envelope contract is locked down in:
proxy/internal/handlers/common/response_test.gocore/core/tests/test_error_envelope.py
The repo validates changes from the root and deploys each app independently.
- Pull requests run
.github/workflows/monorepo-ci.yml, which detects touched paths and finishes with a singleci-summarycheck for branch protection. Changes towebalso run the stable desktop production-build browser smoke. - Weekly and manual
.github/workflows/browser-baseline.ymlruns preserve cold/warm measurements and known-regression artifacts. - Pushes to
maintrigger app-specific deploy workflows: - Each app builds and deploys independently from its own directory.
Start here when you need project context:
.docs/README.md- central documentation index.docs/architecture/current-state.md- current system snapshot.docs/features/implemented.md- shipped capabilities and platform foundations.docs/technical-debt.md- active debt.docs/roadmap/open-plans.md- open and partially implemented work.docs/adr/0001-external-metadata-integration.md- hybrid topologyweb/core/proxy.docs/adr/0002-web-auth-cookies.md- web session / cookie direction.docs/adr/0003-migrate-web-from-nextjs-to-tanstack-start.md- frontend stack (TanStack Start).docs/adr/0004-public-catalog-auth-boundary.md- public catalog and login-on-action boundary.docs/adr/0005-unified-personal-progress.md- unified, type-aware personal progress and independent seasons.docs/contracts/internal-http.md- cross-service contract.docs/observability.md- log schema, request correlation, and alerting guidance.docs/runbooks/browser-e2e-and-baseline.md- production-build browser smoke, regression and baseline procedure.docs/history/implementation-history.md- durable summary of completed sprint outcomesAGENTS.md- contributor and agent rules for the repo