Homebrew RSS reader server

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)

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: <server.api_key>
  • 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#

  1. get_feeds_with_categories(...)
  2. get_feed_with_category(id)
  3. create_feed(feed_url, category_id)
  4. update_feed(id, fields...)
  5. delete_feed(id)
  6. get_category_feeds(category_id)
  7. mark_all_feed_entries_read(feed_id)
  8. mark_all_category_entries_read(category_id)
  9. get_feed_counters() -> Miniflux counters shape

Entries#

  1. get_entry(id)
  2. get_entries_filtered(filter) -> (total, Vec<Entry>)
  3. update_entries_status(entry_ids, status)
  4. toggle_entry_starred(id)

Icons#

  1. get_icon_by_id(id)
  2. 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:

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.