# Miniflux v2 API Cutover Plan (Fever Removed, DB Source of Truth) ## Decisions (Locked In) 1. **Remove Fever API immediately** (no dual-stack period). 2. **Expose only Miniflux-compatible API endpoints**. 3. **SQLite is the source of truth** for categories/feeds/items. 4. `slurp.toml` becomes **runtime config only** (bind, db path, fetch interval, auth token). 5. Existing `[[groups]]` / `[[feeds]]` in `slurp.toml` are treated as **legacy bootstrap input only** (one-time import), then ignored/removed. --- ## Current State Summary - **Schema**: `groups`, `feeds`, `items`, `favicons` - **API**: Fever-only handler on `/`, `/fever`, `/fever/` - **Auth**: Fever-style md5 API key in POST body - **Config behavior**: `db::sync_config()` enforces config as source of truth (including deletions) - **Gaps for Miniflux**: - no `is_starred` - no Miniflux `/v1/*` routes - favicon format stored as combined `"mime;base64,DATA"` --- ## Step 1: Database Migrations ### 1a) Add starred state **File**: `migrations/003_starred.sql` Add: - `items.is_starred INTEGER NOT NULL DEFAULT 0 CHECK(is_starred IN (0,1))` - index on `items(is_starred)` ### 1b) Add change tracking timestamp (recommended) **File**: `migrations/004_changed_at.sql` Add: - `items.changed_at INTEGER NOT NULL DEFAULT (unixepoch())` - index on `items(changed_at)` `changed_at` should be updated whenever read/starred status changes. --- ## Step 2: Move Source of Truth from Config to DB ### 2a) Stop destructive config sync **File**: `src/main.rs` - Remove call to `db::sync_config(&pool, &config)` in `serve`. - Replace with one-time legacy bootstrap: - if DB has zero groups/feeds and config still contains legacy sections, import them. - otherwise do nothing. ### 2b) Update config model **File**: `src/config.rs` - Keep runtime config fields: - `server.bind` - `server.api_key` (used as Miniflux auth token) - `database.path` - `fetcher.interval_minutes` - Mark `groups`/`feeds` as deprecated legacy bootstrap input. ### 2c) Add bootstrap helper **File**: `src/db.rs` Add `bootstrap_from_legacy_config_if_empty(...)`: - If DB empty, insert groups + feeds from config. - Never delete DB rows based on config. --- ## Step 3: Remove Fever Completely Delete/remove: - `src/api/fever.rs` - Fever routes in `src/server.rs` - Fever test files: - `tests/fever_test.rs` - `tests/fever_integration.rs` - Fever-specific comments/types in `src/db.rs` naming - md5-focused CLI behavior tied only to Fever compatibility **Result**: No `/fever` and no Fever request handling. --- ## Step 4: Auth Model for Miniflux **Files**: - `src/api/miniflux/auth.rs` (new) - `src/config.rs` Implement an auth extractor/middleware that accepts: - `X-Auth-Token: ` - HTTP Basic (`username:password`) where `password == server.api_key` On failure, return: - `401 Unauthorized` - JSON: `{"error_message":"Access Unauthorized"}` --- ## Step 5: Miniflux API Types **File**: `src/api/miniflux/types.rs` (new) Define serializable response structs for: - `/v1/me` - categories - feeds (with nested category/icon) - entries + entries envelope (`{ total, entries }`) - icons - counters (`{ reads: {feed_id: count}, unreads: {feed_id: count} }`) - version payload Use sane defaults for unsupported Miniflux fields (`""`, `false`, `null`, etc.). --- ## Step 6: DB Query/Command Layer for Miniflux **File**: `src/db.rs` Add/adjust functions: ### Categories 1. `get_categories(...)` 2. `get_category(...)` 3. `create_category(title)` 4. `update_category(id, title)` 5. `delete_category(id)` 6. `get_category_counts(...)` (`feed_count`, `total_unread`) ### Feeds 7. `get_feeds_with_categories(...)` 8. `get_feed_with_category(id)` 9. `create_feed(feed_url, category_id)` 10. `update_feed(id, fields...)` 11. `delete_feed(id)` 12. `get_category_feeds(category_id)` 13. `mark_all_feed_entries_read(feed_id)` 14. `mark_all_category_entries_read(category_id)` 15. `get_feed_counters()` -> Miniflux counters shape ### Entries 16. `get_entry(id)` 17. `get_entries_filtered(filter) -> (total, Vec)` 18. `update_entries_status(entry_ids, status)` 19. `toggle_entry_starred(id)` ### Icons 20. `get_icon_by_id(id)` 21. `get_icon_by_feed_id(feed_id)` ### Query Builder constraints - Build dynamic filters with `sqlx::QueryBuilder` - Whitelist `order` and `direction` - Clamp `limit` to a max (e.g. 500) --- ## Step 7: Implement Miniflux Handlers **File**: `src/api/miniflux/handlers.rs` (new) ### 7a) User - `GET /v1/me` ### 7b) Categories (now writable) - `GET /v1/categories` (`?counts=true` support) - `POST /v1/categories` - `PUT /v1/categories/:id` - `DELETE /v1/categories/:id` - `PUT /v1/categories/:id/mark-all-as-read` - `GET /v1/categories/:id/entries` - `GET /v1/categories/:id/feeds` ### 7c) Feeds (now writable) - `GET /v1/feeds` - `GET /v1/feeds/:id` - `POST /v1/feeds` (add feed through API) - `PUT /v1/feeds/:id` - `DELETE /v1/feeds/:id` - `GET /v1/feeds/:id/icon` - `GET /v1/feeds/:id/entries` - `PUT /v1/feeds/:id/mark-all-as-read` - `PUT /v1/feeds/:id/refresh` - `PUT /v1/feeds/refresh` ### 7d) Entries - `GET /v1/entries` - `GET /v1/entries/:id` - `PUT /v1/entries` (`{ entry_ids, status }`) - `PUT /v1/entries/:id/bookmark` ### 7e) Icons - `GET /v1/icons/:id` ### 7f) OPML - `GET /v1/export` - (optional) `POST /v1/import` ### 7g) Health/Version - `GET /healthcheck` (root path) - `GET /liveness` and `/healthz` (root paths) - `GET /readiness` and `/readyz` (root paths) - `GET /v1/version` - `GET /v1/feeds/counters` --- ## Step 8: Router Wiring **Files**: - `src/api/miniflux/mod.rs` (new) - `src/api/mod.rs` - `src/server.rs` Target shape: ```rust Router::new() .nest("/v1", miniflux_router()) .route("/healthcheck", get(...)) .route("/liveness", get(...)) .route("/healthz", get(...)) .route("/readiness", get(...)) .route("/readyz", get(...)) .with_state(state) ``` No Fever routes are registered. --- ## Step 9: Fetcher Trigger Integration **Files**: `src/fetcher.rs`, `src/main.rs`, handlers - Keep periodic fetch loop. - Add an internal trigger mechanism for: - `PUT /v1/feeds/:id/refresh` - `PUT /v1/feeds/refresh` - Ensure manual refresh does not race badly with periodic runs. --- ## Step 10: CLI Cleanup **File**: `src/main.rs` - Remove Fever-specific md5 auth expectations. - `Auth` subcommand should be removed or repurposed to generate a random token. - `Add`/`Import` should write to DB (or be removed if API-first is preferred). --- ## Step 11: Testing 1. Remove Fever tests. 2. Add Miniflux integration tests for: - auth (`X-Auth-Token`, Basic) - feed/category CRUD - entries filtering and pagination - status updates and bookmark toggle - counters shape - health/version endpoints 3. Verify with real client against `/v1`. --- ## File Layout After Cutover ``` src/ api/ mod.rs ← only `pub mod miniflux;` miniflux/ mod.rs auth.rs types.rs handlers.rs config.rs db.rs fetcher.rs server.rs main.rs migrations/ 001_initial.sql 002_read_status.sql 003_starred.sql 004_changed_at.sql tests/ miniflux_*.rs ``` --- ## Suggested Implementation Order 1. Migrations (`003`, `004`) 2. Remove Fever routes/files/tests 3. Stop `sync_config` as source of truth; add bootstrap-if-empty 4. Add Miniflux auth extractor 5. Add Miniflux types + DB functions 6. Implement handlers (user → categories → feeds → entries → icons → counters) 7. Wire router + health/version endpoints 8. Fetch trigger integration 9. CLI cleanup 10. Integration testing with real client This delivers a clean cutover: **Miniflux-only API with DB-owned feeds/categories and no Fever compatibility layer**.