Files
SFusion/FUSION_REPOSITORY_ANALYSIS.md
2026-05-09 13:28:12 +03:00

17 KiB
Raw Permalink Blame History

Fusion (0x2e/fusion) — Repository Analysis

This report summarizes the Fusion self-hosted RSS reader: its stack, dependencies, and a practical roadmap to reimplement parity using Vapor (Swift) for the backend and React for the frontend.


1. What Fusion Is

  • Product: Lightweight RSS/Atom reader with unread tracking, bookmarks, search, keyboard shortcuts, groups, and PWA support.
  • Distribution: Single Go binary that embeds the built SPA, or Docker / Fly.io; data in SQLite.
  • Compatibility: Fever API for third-party clients (Reeder, Unread, FeedMe, etc.).
  • Auth: Password sessions (cookie session) plus optional OIDC SSO.
  • Contract: HTTP JSON API under /api documented in docs/openapi.yaml.

2. Technology Stack (Upstream)

Layer Technology
Backend language Go (module declares go 1.26; README/dev docs mention 1.25+)
HTTP Gin
Database SQLite via modernc.org/sqlite (pure Go, no CGO in release builds)
Migrations Embedded .sql files
Feed parsing gofeed
Feed discovery feedfinder
Auth golang.org/x/crypto (password hashing), go-oidc, golang.org/x/oauth2
Frontend React 19, TypeScript, Vite 7
Routing TanStack Router
Server state / cache TanStack Query
Local UI state Zustand
Styling Tailwind CSS 4, shadcn/ui, Radix primitives
Other FE DOMPurify (HTML sanitization), next-themes, Lucide icons, Sonner toasts, cmdk (command palette)

Build pipeline: scripts.sh runs pnpm in frontend/, copies dist/ into backend/internal/web/dist/, then go build embeds the static assets into one binary.


3. Backend Dependencies (backend/go.mod)

Direct require entries:

Module Role
github.com/0x2E/feedfinder Discover RSS/Atom URLs from a site
github.com/coreos/go-oidc/v3 OIDC token validation
github.com/gin-gonic/gin HTTP router/middleware
github.com/google/uuid IDs (sessions, etc.)
github.com/mattn/go-isatty TTY detection (logging/CLI)
github.com/mmcdole/gofeed RSS/Atom parsing
golang.org/x/crypto Password hashing
golang.org/x/oauth2 OAuth2 client for OIDC
golang.org/x/sync Concurrency helpers (e.g. errgroup/limits)
modernc.org/sqlite SQLite driver

Go pulls a standard set of indirect dependencies (JSON binding, validation, HTTP/2 QUIC stack used by Gins stack, etc.) — see the full require block in the repo for the exact pinned versions.


4. Frontend Dependencies (frontend/package.json)

Runtime (dependencies) — highlights:

  • React: react, react-dom (~19.2)
  • Build-time CSS: @tailwindcss/vite, tailwindcss, tw-animate-css
  • Data: @tanstack/react-query, @tanstack/react-router
  • State: zustand
  • UI: extensive @radix-ui/*, shadcn, class-variance-authority, clsx, tailwind-merge, cmdk, sonner, lucide-react, @fontsource-variable/inter
  • Security / content: dompurify

Dev (devDependencies): TypeScript, ESLint, Vite, @vitejs/plugin-react, TanStack Router CLI/plugin, Tailwind typography, type packages.

Package manager: pnpm (lockfile: frontend/pnpm-lock.yaml).


5. Functional Surface You Must Reproduce

5.1 REST API (/api/*)

Source of truth: docs/openapi.yaml. Areas include:

  • Sessions: POST /api/sessions (login), DELETE /api/sessions (logout); session cookie auth for protected routes.
  • OIDC: /api/oidc/enabled, /api/oidc/login, /api/oidc/callback (redirect flow).
  • CRUD: groups, feeds (including validate, batch create, refresh), items (list, mark read/unread), bookmarks.
  • Search: feed + item search (backend uses SQLite FTS5 on items).

JSON envelope and error shapes are defined in OpenAPI — match them if you want a drop-in React client.

5.2 Fever API

  • POST /fever, /fever/, /fever.php with application/x-www-form-urlencoded.
  • Auth: api_key = md5(username:password) per docs/fever-api.md.
  • Optional parity for mobile/desktop clients.

5.3 Background work: feed puller

The Go process runs two long-lived concerns in one binary:

  1. HTTP server
  2. Periodic + manual feed fetch worker: concurrency limits, timeouts, conditional requests (ETag, Last-Modified), backoff, Retry-After, cache headers, per-feed feed_fetch_state (see docs/backend-design.md).

Any Vapor port needs the same scheduling semantics (or document differences).

5.4 Data model

  • groups, feeds, feed_fetch_state, items, bookmarks, items_fts (FTS5 + triggers), schema migrations 001 / 002 under backend/internal/store/migrations/.
  • Legacy DB import path exists in Go; a greenfield Vapor app can start from the current schema only.

5.5 Security / ops behavior

  • SSRF-style protections for feed URLs (private IP blocking unless explicitly allowed) — see internal/pkg/httpc patterns in the Go tree.
  • CORS, trusted proxies, login rate limiting, logging — env-driven (see .env.example in the repo: FUSION_* variables).

6. Reproducing the Project with Vapor (Swift) + React

This is a full reimplementation, not a thin wrapper. The sections below list what to build and which Swift/React ecosystem pieces typically map to the Go/TS stack.

6.1 Backend (Vapor)

  1. HTTP API parity

    • Implement routes and JSON models to match docs/openapi.yaml (or generate Swift types from OpenAPI with a code generator and fill in handlers).
    • Session middleware: issue/validate session cookie named session; match login rate limits and 401 behavior expected by the existing frontend.
  2. SQLite

    • Use Fluent + SQLite or raw SQL via SQLite.swift / GRDB — Fusion relies on FTS5 and triggers; confirm your Swift SQLite build enables FTS5.
    • Port migrations from 001_initial.sql and 002_feed_fetch_state.sql (and any later migrations in the repo).
    • Reproduce cascade / move rules** for group/feed deletion** as documented in docs/backend-design.md (explicit transactions, not only ON DELETE).
  3. Feed ingestion

    • Parse: FeedKit or similar for RSS/Atom.
    • Discovery: port or call feed discovery behavior equivalent to feedfinder (HTTP fetch + HTML link rel discovery).
    • HTTP client: custom timeouts, redirect limits, and private-IP blocking mirroring Fusions policy unless you intentionally differ.
  4. Background scheduler

    • Run the pull loop in a Vapor Application lifecycle task** or a separate Swift executable that shares the DB file (single-writer caution: use one writer or WAL with careful locking).
    • Implement the same interval, concurrency, timeout, max backoff, conditional GET, and feed_fetch_state updates described in backend design docs.
  5. Auth

    • Password: hash with a modern KDF (Argon2id or bcrypt) — match whatever the OpenAPI responses assume for “invalid password” flows.
    • OIDC: use an OAuth2/OIDC Swift client; implement the same redirect URI contract (/api/oidc/callback).
  6. Fever (optional but for full parity)

    • Add form-encoded POST handlers and response JSON matching Fever clients expectations (docs/fever-api.md).
  7. Static hosting

    • Either serve the React build from Vapors Public/FileMiddleware (like the embedded Go dist/) or deploy the SPA to a CDN and set VITE_API_BASE_URL (or equivalent) to the Vapor origin.
  8. Configuration

    • Mirror FUSION_* semantics from .env.example (port, DB path, CORS, pull tuning, OIDC, Fever username, etc.) using Vapors environment layer.

6.2 Frontend (React)

You have two approaches:

Approach Effort Outcome
Port the existing UI High Rebuild routes/components; upstream uses TanStack Router — map to React Router v6+ or keep TanStack Router.
New React app, same API MediumHigh Generate a TypeScript client from openapi.yaml; reimplement layout (sidebar, drawer, keyboard shortcuts, i18n, PWA).

Must-haves for behavioral parity (from docs/frontend-design.md and the code):

  • URL-driven state: /:filter, /feeds/:feedId/:filter, /groups/:groupId/:filter, ?article= for drawer.
  • API client: fetch with credentials: "include" and base URL /api or VITE_API_BASE_URL.
  • Features: unread/all/starred, infinite list, mark all read, OPML import/export logic (see frontend/src/lib/opml.ts), bookmarks, search, settings, themes, i18n (multiple locale files under frontend/src/lib/i18n/).
  • PWA: service worker / manifest equivalents (frontend/public/).
  • Sanitization: DOMPurify (or equivalent) for article HTML.

Dependency mapping (optional):

  • TanStack Query → keeps React Query (recommended for parity with caching patterns).
  • Zustand → same or Redux/context for minimal UI prefs.
  • shadcn + Tailwind → same stack works in any Vite/React app.

6.3 Testing parity

  • Contract tests: run the OpenAPI spec through Dredd/Schemathesis or manually curl against Vapor.
  • Fever: use the curl example in docs/fever-api.md.
  • Feed pull: integration tests with mock HTTP servers for 200/304, Retry-After, and failure backoff.

6.4 Estimated complexity

  • Backend: largest effort (scheduler + HTTP cache semantics + FTS + auth/OIDC + Fever).
  • Frontend: large if you duplicate every screen; smaller if you reuse design docs and OpenAPI but simplify UI.
  • Risk: SQLite file locking if you split “API server” and “worker” into two processes without a clear WAL/queue strategy.

7. Reference Files in the Upstream Repo

Path Purpose
docs/openapi.yaml API contract
docs/backend-design.md DB, pull algorithm, modules
docs/frontend-design.md Routes, UX, state
docs/fever-api.md Fever compatibility
backend/go.mod Go dependencies
frontend/package.json JS dependencies
scripts.sh Build/embed pipeline
.env.example Configuration surface

8. Summary

Fusion is a Go + embedded React app with SQLite (FTS5), a session/OIDC API under /api, optional Fever endpoints, and a background feed poller with nuanced HTTP caching and backoff. To reproduce it in Vapor and React, treat docs/openapi.yaml and docs/backend-design.md as the specification: implement the same data model and pull semantics in Swift, expose identical HTTP behavior, then either port or rewrite the React client against that contract — preserving cookie auth, URL structure, and PWA/i18n if full parity is required.

A narrower backend rewrite (Vapor + PostgreSQL, existing React SPA unchanged) is described in section 9.


This section refines the plan: rewrite only the backend with Vapor and PostgreSQL, map Fusions Go dependencies to Swift equivalents, and use sound scheduling patterns for the feed worker.

9.1 Backend-only Vapor + existing React

Keeping the upstream SPA is a strong option. It already targets /api with JSON and session cookies (OpenAPI). If the Vapor app matches that contract (and Fever routes if mobile clients matter), you do not need to rewrite the frontend. Configure CORS, cookie SameSite, and reverse-proxy headers the same way Fusion documents for split origins (SPA on one host, API on another).

9.2 PostgreSQL instead of SQLite

SQLite (Fusion) PostgreSQL
FTS5 + triggers on items tsvector + GIN index, or a GENERATED column + index; keep rows in sync via trigger (closest to Fusion) or on write in application code
Single file, typical single-writer Connection pool; Fluent migrations or versioned SQL
Unix-epoch integers in schema Prefer timestamptz where wall time matters; you can still expose epoch ints in JSON for API compatibility

PostgreSQL full-text search is the natural replacement for FTS5. Triggers preserve the same “index always matches items” invariant as Fusions SQLite triggers.

9.3 Swift / Vapor analogs for Go backend dependencies

Go (Fusion) Swift / Vapor role
Gin Vapor routing, middleware, Request / Response
modernc.org/sqlite Fluent + fluent-postgres-driver, or postgres-nio / SQLKit if you prefer raw SQL
Embedded SQL migrations Fluent migrations, SQL executed from migrations, or external migration tooling — the pattern stays valid
gofeed FeedKit (RSS/Atom); validate real-world feeds (encoding, CDATA)
feedfinder No common 1:1 Swift package; async-http-client + SwiftSoup (or similar) for HTML and <link rel="alternate">; optional small port of the same discovery rules
go-oidc + oauth2 Provider-specific or generic OAuth2/OIDC flows; validate tokens / JWKS (e.g. JWTKit) depending on provider; exact stack follows your IdP
golang.org/x/crypto (passwords) BCrypt or Argon2 Swift bindings; align cost parameters and any password-migration story
google/uuid Foundation UUID
Session cookie Vapor Sessions with a store appropriate for production (Postgres or Redis)
Fever No extra framework: application/x-www-form-urlencoded, MD5 api_key, response DTOs per docs/fever-api.md

9.4 Scheduled tasks and feed polling (best practice on Vapor)

Fusions worker is database-driven, not “one cron job per feed”: each feed has next_check_at (and related state); a loop selects due feeds, respects concurrency, timeouts, and backoff, then updates rows.

Recommended layering

  1. Driver tick — One scheduled job (or a short-interval repeat) runs every N seconds (e.g. 3060s). It does not schedule per URL in memory.
  2. Work selection — Query feeds that are due (next_check_at, retry_after_until, suspended, etc.), same semantics as docs/backend-design.md.
  3. Concurrency — Cap parallel fetches with TaskGroup (or equivalent) to mirror FUSION_PULL_CONCURRENCY.

Libraries: Vapor Queues

  • Job types, retries, startScheduledJobs() for cron-like recurrence, workers as separate processes (e.g. swift run App queues).
  • Production often uses the official Redis driver for the queue backend.
  • Community Fluent/Postgres queue drivers exist if you want the database as the queue (e.g. SKIP LOCKED for competing workers) and fewer moving parts than Redis.

How Queues fits Fusion

  • Queues helps with orchestration: retries, worker processes, optional horizontal scale.
  • Postgres (per-feed next_check_at, feed_fetch_state, cache metadata) remains the source of truth for when and how to pull — same as upstream; do not rely on the queue alone to remember HTTP caching rules.

Patterns to avoid

  • One Timer per feed in process memory — does not survive restarts and diverges from Fusions DB-backed schedule.
  • Blocking the request thread for long fetches — keep pulls in async services and/or queue workers.

Deployment options

Pattern When to use
Scheduled Queues job (e.g. every minute) + SQL “due feeds” query Closest to “single binary + shared state”
Enqueue RefreshFeedJob(feedId) for each due row Scale multiple workers; Redis or Postgres-backed queue
External cron hitting an internal “trigger pull” route Simple ops; guard with DB advisory lock or a single designated worker

9.5 Summary (Vapor + Postgres path)

  • Vapor backend + PostgreSQL + unchanged React is a sound architecture if docs/openapi.yaml is honored and FTS is implemented as tsvector + GIN (plus triggers or app-level sync).
  • Go → Swift mapping above covers the main backend crates; OIDC is the area where the choice depends on the identity provider, not a mechanical port.
  • Scheduling: treat the puller as DB-scheduled batches; use Vapor Queues (scheduled job + optional Redis/Fluent workers), not per-feed cron or naive timers.

Generated from analysis of github.com/0x2e/fusion (shallow clone for file inspection). Version pins and minor details may change on main; verify the live repo before locking production dependencies.