docs: adr 0012 sqlite only

This commit is contained in:
2026-10-25 20:39:47 +01:00
parent 1b7384386a
commit 3a05b83320
9 changed files with 41 additions and 23 deletions

View File

@@ -51,14 +51,14 @@ domain → pure types, traits (ports), zero deps
application → use cases, orchestration application → use cases, orchestration
presentation → Axum handlers, routes, OpenAPI presentation → Axum handlers, routes, OpenAPI
worker → event consumer, background jobs worker → event consumer, background jobs
adapters/* → implements domain ports (sqlite, postgres, AP, etc.) adapters/* → implements domain ports (sqlite, AP, etc.)
spa/ → React SPA (TanStack Router + shadcn/ui) spa/ → React SPA (TanStack Router + shadcn/ui)
``` ```
### Adding a new feature ### Adding a new feature
1. **Domain first** — models in `domain/src/models/`, ports in `ports.rs`, events in `events.rs` 1. **Domain first** — models in `domain/src/models/`, ports in `ports.rs`, events in `events.rs`
2. **Adapters** — implement ports in both `sqlite` and `postgres` adapters, add migration 2. **Adapters** — implement ports in the `sqlite*` adapters, add migration
3. **Application** — use cases in `application/src/<domain>/`, wire into `context.rs` 3. **Application** — use cases in `application/src/<domain>/`, wire into `context.rs`
4. **API types** — DTOs in `api-types/src/` 4. **API types** — DTOs in `api-types/src/`
5. **Presentation** — handler file in `handlers/<domain>.rs`, routes in `routes.rs` 5. **Presentation** — handler file in `handlers/<domain>.rs`, routes in `routes.rs`
@@ -67,7 +67,7 @@ spa/ → React SPA (TanStack Router + shadcn/ui)
### Database adapters ### Database adapters
Both SQLite and PostgreSQL are supported. If you add a migration or repository, implement it for both. The postgres adapter uses `$1, $2` params and `TIMESTAMPTZ`; SQLite uses `?` and text datetimes. SQLite is the only supported backend (see [ADR-0012](docs/adr/0012-sqlite-is-the-only-backend.md)). Add migrations under the sqlite adapter; SQLite uses `?` params and text datetimes.
### Federation (ActivityPub) ### Federation (ActivityPub)

View File

@@ -36,10 +36,6 @@ COPY crates/adapters/sqlite/Cargo.toml crates/adapters/sqlite/Cargo
COPY crates/adapters/sqlite-federation/Cargo.toml crates/adapters/sqlite-federation/Cargo.toml COPY crates/adapters/sqlite-federation/Cargo.toml crates/adapters/sqlite-federation/Cargo.toml
COPY crates/adapters/sqlite-social/Cargo.toml crates/adapters/sqlite-social/Cargo.toml COPY crates/adapters/sqlite-social/Cargo.toml crates/adapters/sqlite-social/Cargo.toml
COPY crates/adapters/sqlite-event-queue/Cargo.toml crates/adapters/sqlite-event-queue/Cargo.toml COPY crates/adapters/sqlite-event-queue/Cargo.toml crates/adapters/sqlite-event-queue/Cargo.toml
COPY crates/adapters/postgres/Cargo.toml crates/adapters/postgres/Cargo.toml
COPY crates/adapters/postgres-federation/Cargo.toml crates/adapters/postgres-federation/Cargo.toml
COPY crates/adapters/postgres-social/Cargo.toml crates/adapters/postgres-social/Cargo.toml
COPY crates/adapters/postgres-event-queue/Cargo.toml crates/adapters/postgres-event-queue/Cargo.toml
COPY crates/adapters/template-askama/Cargo.toml crates/adapters/template-askama/Cargo.toml COPY crates/adapters/template-askama/Cargo.toml crates/adapters/template-askama/Cargo.toml
COPY crates/api-types/Cargo.toml crates/api-types/Cargo.toml COPY crates/api-types/Cargo.toml crates/api-types/Cargo.toml
COPY crates/application/Cargo.toml crates/application/Cargo.toml COPY crates/application/Cargo.toml crates/application/Cargo.toml
@@ -51,7 +47,6 @@ COPY crates/server/Cargo.toml crates/server/Cargo.toml
COPY crates/tui/Cargo.toml crates/tui/Cargo.toml COPY crates/tui/Cargo.toml crates/tui/Cargo.toml
COPY crates/adapters/image-converter/Cargo.toml crates/adapters/image-converter/Cargo.toml COPY crates/adapters/image-converter/Cargo.toml crates/adapters/image-converter/Cargo.toml
COPY crates/adapters/sqlite-search/Cargo.toml crates/adapters/sqlite-search/Cargo.toml COPY crates/adapters/sqlite-search/Cargo.toml crates/adapters/sqlite-search/Cargo.toml
COPY crates/adapters/postgres-search/Cargo.toml crates/adapters/postgres-search/Cargo.toml
COPY crates/adapters/adapter-common/Cargo.toml crates/adapters/adapter-common/Cargo.toml COPY crates/adapters/adapter-common/Cargo.toml crates/adapters/adapter-common/Cargo.toml
COPY crates/worker/Cargo.toml crates/worker/Cargo.toml COPY crates/worker/Cargo.toml crates/worker/Cargo.toml
COPY crates/infra-wiring/Cargo.toml crates/infra-wiring/Cargo.toml COPY crates/infra-wiring/Cargo.toml crates/infra-wiring/Cargo.toml

View File

@@ -1,12 +1,12 @@
# Movies Diary # Movies Diary
A self-hosted movie diary built in Rust. Ships a classic server-rendered HTML interface (no JavaScript) alongside a full React SPA, both backed by the same REST API. Federates over ActivityPub so reviews reach the Fediverse. Supports Jellyfin and Plex auto-import, full-text search, annual wrap-ups, goals, and bulk import from Letterboxd, IMDb, and other sources. Runs on SQLite or PostgreSQL. A self-hosted movie diary built in Rust. Ships a classic server-rendered HTML interface (no JavaScript) alongside a full React SPA, both backed by the same REST API. Federates over ActivityPub so reviews reach the Fediverse. Supports Jellyfin and Plex auto-import, full-text search, annual wrap-ups, goals, and bulk import from Letterboxd, IMDb, and other sources. Runs on SQLite.
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Built with Rust](https://img.shields.io/badge/built_with-Rust-orange.svg?logo=rust)](https://www.rust-lang.org/) [![Built with Rust](https://img.shields.io/badge/built_with-Rust-orange.svg?logo=rust)](https://www.rust-lang.org/)
[![Docker](https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/) [![Docker](https://img.shields.io/badge/docker-ready-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/)
[![ActivityPub](https://img.shields.io/badge/ActivityPub-federated-5b5ea6)](https://activitypub.rocks/) [![ActivityPub](https://img.shields.io/badge/ActivityPub-federated-5b5ea6)](https://activitypub.rocks/)
[![SQLite](https://img.shields.io/badge/database-SQLite%20%7C%20PostgreSQL-003B57?logo=sqlite&logoColor=white)](https://www.sqlite.org/) [![SQLite](https://img.shields.io/badge/database-SQLite-003B57?logo=sqlite&logoColor=white)](https://www.sqlite.org/)
--- ---
@@ -52,7 +52,7 @@ Open `http://localhost:3000`. The HTTP server and background worker start togeth
- Spoiler Flag — authors mark a review as containing spoilers; the comment is veiled (blur + reveal) for every viewer except the author, withheld from RSS, and federates as `containsSpoilers` + an AP `summary` content warning so Mastodon-style clients also collapse it - Spoiler Flag — authors mark a review as containing spoilers; the comment is veiled (blur + reveal) for every viewer except the author, withheld from RSS, and federates as `containsSpoilers` + an AP `summary` content warning so Mastodon-style clients also collapse it
- Background poster fetching and storage (local filesystem or S3-compatible) - Background poster fetching and storage (local filesystem or S3-compatible)
- Movie enrichment via TMDb — full cast, crew, genres, keywords, runtime, budget/revenue, ratings; fetched automatically on movie discovery and refreshed every 30 days; exposed via `GET /api/v1/movies/{id}/profile` - Movie enrichment via TMDb — full cast, crew, genres, keywords, runtime, budget/revenue, ratings; fetched automatically on movie discovery and refreshed every 30 days; exposed via `GET /api/v1/movies/{id}/profile`
- Full-text search across movies and people via `GET /api/v1/search` — free-text query plus structured filters (genre, year, person, department, language); backed by SQLite FTS5 or PostgreSQL tsvector + GIN indexes - Full-text search across movies and people via `GET /api/v1/search` — free-text query plus structured filters (genre, year, person, department, language); backed by SQLite FTS5
- People as first-class entities — browse by person via `GET /api/v1/people/{id}` and full credit history via `GET /api/v1/people/{id}/credits`; index populated automatically during TMDb enrichment - People as first-class entities — browse by person via `GET /api/v1/people/{id}` and full credit history via `GET /api/v1/people/{id}/credits`; index populated automatically during TMDb enrichment
- RSS/Atom feed for public subscription (global and per-user) - RSS/Atom feed for public subscription (global and per-user)
- JWT authentication via cookie (HTML) or Bearer token (REST API) - JWT authentication via cookie (HTML) or Bearer token (REST API)
@@ -95,13 +95,12 @@ infra-wiring — shared infrastructure types (DbPool, EventBusBackend, Ap
domain — pure types and CQRS port traits (MovieCommand/MovieQuery, WatchEventCommand/WatchEventQuery, GoalCommand/GoalQuery, DiaryQuery, PersonCommand/PersonQuery, SearchCommand/SearchPort, SocialCommand/SocialQuery, ImageFetcher, RssFeedRenderer), no external deps except serde domain — pure types and CQRS port traits (MovieCommand/MovieQuery, WatchEventCommand/WatchEventQuery, GoalCommand/GoalQuery, DiaryQuery, PersonCommand/PersonQuery, SearchCommand/SearchPort, SocialCommand/SocialQuery, ImageFetcher, RssFeedRenderer), no external deps except serde
application — use cases (commands + queries), business logic orchestration; handlers delegate here for all domain logic; modules: auth, diary, goals, import, integrations, movies, person, search, social, users, watchlist, wrapup application — use cases (commands + queries), business logic orchestration; handlers delegate here for all domain logic; modules: auth, diary, goals, import, integrations, movies, person, search, social, users, watchlist, wrapup
presentation — Axum HTTP router, OpenAPI spec assembly, Swagger UI + Scalar serving; library crate, no binary of its own presentation — Axum HTTP router, OpenAPI spec assembly, Swagger UI + Scalar serving; library crate, no binary of its own
server — owns the HTTP binary, backend-selection features (sqlite/postgres, federation), boots presentation's router server — owns the HTTP binary, features (federation, nats), boots presentation's router
worker — standalone worker binary (event consumer, poster sync, federation) worker — standalone worker binary (event consumer, poster sync, federation)
adapters/ adapters/
adapter-common — shared row-to-domain conversions, sqlx error mapping, date/uuid parsing utils adapter-common — shared row-to-domain conversions, sqlx error mapping, date/uuid parsing utils
auth — JWT issuance and validation (Argon2 passwords) auth — JWT issuance and validation (Argon2 passwords)
sqlite — SQLite repository + connection factory sqlite — SQLite repository + connection factory
postgres — PostgreSQL repository + connection factory
metadata — TMDB / OMDb HTTP client metadata — TMDB / OMDb HTTP client
poster-fetcher — downloads poster images poster-fetcher — downloads poster images
object-storage — stores images (posters + user avatars) on local filesystem or S3-compatible storage object-storage — stores images (posters + user avatars) on local filesystem or S3-compatible storage
@@ -116,15 +115,12 @@ adapters/
plex — Plex webhook payload parser (MediaServerParser adapter; requires Plex Pass) plex — Plex webhook payload parser (MediaServerParser adapter; requires Plex Pass)
event-payload — shared event serialization DTOs (used by all event bus adapters) event-payload — shared event serialization DTOs (used by all event bus adapters)
sqlite-event-queue — durable polling event queue backed by SQLite sqlite-event-queue — durable polling event queue backed by SQLite
postgres-event-queue — durable polling event queue backed by PostgreSQL
event-publisher — in-memory event channel (used in tests) event-publisher — in-memory event channel (used in tests)
nats — NATS Core / JetStream event publisher and consumer nats — NATS Core / JetStream event publisher and consumer
event-publisher — in-memory event channel (used in tests) event-publisher — in-memory event channel (used in tests)
activitypub — ActivityPub federation adapter (follow, inbox/outbox, actor); delegates to k-ap for protocol internals activitypub — ActivityPub federation adapter (follow, inbox/outbox, actor); delegates to k-ap for protocol internals
sqlite-search — SQLite FTS5 implementation of SearchPort + SearchCommand sqlite-search — SQLite FTS5 implementation of SearchPort + SearchCommand
postgres-search — PostgreSQL tsvector + GIN implementation of SearchPort + SearchCommand
sqlite-federation — SQLite-backed federation repository sqlite-federation — SQLite-backed federation repository
postgres-federation — PostgreSQL-backed federation repository
tui — terminal UI client (ratatui); shares api-types with presentation for typed API access tui — terminal UI client (ratatui); shares api-types with presentation for typed API access
spa/ — React SPA (TanStack Router + shadcn/ui + Vite); served at /app/ by the backend spa/ — React SPA (TanStack Router + shadcn/ui + Vite); served at /app/ by the backend
``` ```
@@ -142,7 +138,7 @@ Copy `.env.example` to `.env` and set the values below. Required fields must be
| Variable | Default | Required | Description | | Variable | Default | Required | Description |
|---|---|---|---| |---|---|---|---|
| `DATABASE_URL` | `sqlite://movies.db` | Yes | SQLite or PostgreSQL connection string | | `DATABASE_URL` | `sqlite://movies.db` | Yes | SQLite connection string |
| `JWT_SECRET` | — | Yes | Secret for JWT signing — use a long random string | | `JWT_SECRET` | — | Yes | Secret for JWT signing — use a long random string |
| `OMDB_API_KEY` | — | Yes | [OMDb](https://www.omdbapi.com/apikey.aspx) key for movie metadata | | `OMDB_API_KEY` | — | Yes | [OMDb](https://www.omdbapi.com/apikey.aspx) key for movie metadata |
| `TMDB_API_KEY` | — | No | [TMDb](https://www.themoviedb.org/settings/api) key — enables cast, crew, genres, enrichment | | `TMDB_API_KEY` | — | No | [TMDb](https://www.themoviedb.org/settings/api) key — enables cast, crew, genres, enrichment |
@@ -260,7 +256,7 @@ docker run -p 3000:3000 --env-file .env -v movies-diary-data:/data movies-diary
docker run --env-file .env -v movies-diary-data:/data --entrypoint ./worker movies-diary docker run --env-file .env -v movies-diary-data:/data --entrypoint ./worker movies-diary
``` ```
Build for PostgreSQL: `--build-arg FEATURES=postgres,postgres-federation` Default build enables `federation`; add NATS with `--build-arg FEATURES=federation,nats`. SQLite is the only database backend (see [ADR-0012](docs/adr/0012-sqlite-is-the-only-backend.md)).
## Media Server Integration ## Media Server Integration

View File

@@ -86,15 +86,12 @@ graph TB
A_COMMON["adapter-common<br/><i>Shared row conversions,<br/>error mapping, date utils</i>"] A_COMMON["adapter-common<br/><i>Shared row conversions,<br/>error mapping, date utils</i>"]
subgraph Storage["Storage"] subgraph Storage["Storage"]
A_SQLITE["sqlite<br/><i>SQLite repos</i>"] A_SQLITE["sqlite<br/><i>SQLite repos</i>"]
A_PG["postgres<br/><i>PostgreSQL repos</i>"]
A_SQLITE_SEARCH["sqlite-search<br/><i>FTS5</i>"] A_SQLITE_SEARCH["sqlite-search<br/><i>FTS5</i>"]
A_PG_SEARCH["postgres-search<br/><i>tsvector/GIN</i>"]
A_OBJ["object-storage<br/><i>S3 / filesystem</i>"] A_OBJ["object-storage<br/><i>S3 / filesystem</i>"]
end end
subgraph Messaging["Messaging"] subgraph Messaging["Messaging"]
A_NATS["nats<br/><i>JetStream / Core</i>"] A_NATS["nats<br/><i>JetStream / Core</i>"]
A_SQLITE_QUEUE["sqlite-event-queue<br/><i>Polling, dead-letter</i>"] A_SQLITE_QUEUE["sqlite-event-queue<br/><i>Polling, dead-letter</i>"]
A_PG_QUEUE["postgres-event-queue<br/><i>Polling, dead-letter</i>"]
A_EVT_PUB["event-publisher<br/><i>In-memory (tests)</i>"] A_EVT_PUB["event-publisher<br/><i>In-memory (tests)</i>"]
A_PAYLOAD["event-payload<br/><i>Serde (de)serialization</i>"] A_PAYLOAD["event-payload<br/><i>Serde (de)serialization</i>"]
end end
@@ -107,7 +104,6 @@ graph TB
subgraph Federation["Federation (feature-gated)"] subgraph Federation["Federation (feature-gated)"]
A_AP["activitypub<br/><i>k_ap library</i>"] A_AP["activitypub<br/><i>k_ap library</i>"]
A_SQLITE_FED["sqlite-federation"] A_SQLITE_FED["sqlite-federation"]
A_PG_FED["postgres-federation"]
end end
subgraph Media["Media Processing"] subgraph Media["Media Processing"]
A_IMG["image-converter<br/><i>AVIF/WebP</i>"] A_IMG["image-converter<br/><i>AVIF/WebP</i>"]

View File

@@ -7,7 +7,7 @@ license = "MIT"
[features] [features]
default = ["federation"] default = ["federation"]
# Meta-feature: true when any federation adapter is active — keeps all #[cfg(feature = "federation")] gates working # Enables ActivityPub federation (pulls in sqlite-federation + activitypub); keeps all #[cfg(feature = "federation")] gates working
federation = ["application/federation"] federation = ["application/federation"]
[dependencies] [dependencies]

View File

@@ -150,3 +150,7 @@ Removing `repos` now would break every one of those 40 sites for no gain this pl
it transitively via presentation/worker's default features` — pre-existing, not introduced or it transitively via presentation/worker's default features` — pre-existing, not introduced or
fixed by this plan, and invisible to `make check`'s workspace-wide `cargo test`, which enables fixed by this plan, and invisible to `make check`'s workspace-wide `cargo test`, which enables
the feature transitively. the feature transitively.
---
Superseded in part by [ADR-0012](0012-sqlite-is-the-only-backend.md): the postgres backends, crates and features mentioned above no longer exist; SQLite is the only backend.

View File

@@ -259,3 +259,7 @@ recorded under Known follow-ups below, which is also pre-existing.
established as legitimate — test helpers build a real `Repositories` and call `composition::build_deps`. established as legitimate — test helpers build a real `Repositories` and call `composition::build_deps`.
`check-presentation-adapters` intentionally does not scan that section, and it does not need to: `check-presentation-adapters` intentionally does not scan that section, and it does not need to:
`composition` is not an adapter crate. `composition` is not an adapter crate.
---
Superseded in part by [ADR-0012](0012-sqlite-is-the-only-backend.md): the postgres backends, crates and features mentioned above no longer exist; SQLite is the only backend.

View File

@@ -365,3 +365,7 @@ still fails with the same two pre-existing errors. This branch neither fixed nor
leaked into `presentation`. ADR-0008's caveat carries over unchanged: the guard assumes directory leaked into `presentation`. ADR-0008's caveat carries over unchanged: the guard assumes directory
name equals package name, true for all 28 today but not enforced. name equals package name, true for all 28 today but not enforced.
- The deferred items in ADR-0005/0006/0007/0008 are otherwise unaffected by this plan. - The deferred items in ADR-0005/0006/0007/0008 are otherwise unaffected by this plan.
---
Superseded in part by [ADR-0012](0012-sqlite-is-the-only-backend.md): the postgres backends, crates and features mentioned above no longer exist; SQLite is the only backend.

View File

@@ -0,0 +1,19 @@
# SQLite is the only database backend
Movies Diary supports exactly one database: SQLite. The PostgreSQL adapters and the `DATABASE_BACKEND` runtime switch are removed.
## Context
The repository carried two adapter families, `sqlite*` and `postgres*`, across five crate pairs (base repositories, social, federation, search, event queue). They were roughly 80% copy-pasted, about 6,000 lines, differing mainly in placeholder syntax (`?` vs `$1`), datetime types and FTS (FTS5 vs tsvector). Every port change had to be made twice. The postgres family had no tests, so it was never verified to work, and the owner has only ever run SQLite. It was dead weight that doubled the maintenance cost of every storage change and the number of feature combinations to build.
## Decision
Delete the `postgres*` crates and the backend switch. SQLite is the sole implementation of the storage ports. If a second backend is ever needed, it is added back deliberately, with tests, behind the existing ports rather than kept alive speculatively.
## Consequences
- One adapter family to change per port; roughly 6,000 fewer lines and five fewer crates to build and review.
- `DATABASE_BACKEND` is ignored. If it is set to anything other than `sqlite`, startup logs one warning ("DATABASE_BACKEND is ignored; SQLite is the only backend") and continues.
- Cargo features collapse to `federation` (default) and `nats`. `sqlite` and `sqlite-federation` remain as deprecated aliases so existing deploy commands (`FEATURES=sqlite,sqlite-federation,nats`) still build. They may be removed in a future release once deploy scripts no longer pass them.
- Existing PostgreSQL deployments are not supported and have no migration path from this repository.
- ADR-0006, ADR-0008 and ADR-0009 mention postgres features and crates; they carry a note that they are superseded in part by this ADR and are otherwise left as written.