# Folo Admin & Developer Guide

This guide is for the people who build, operate, deploy, and extend Folo: maintainers,
self-hosting developers, contributors, and architects evaluating the codebase. It
documents the monorepo's structure, the multi-surface architecture, the shared internal
packages, the data and state model, build and deployment pipelines, security posture, and
the technical debt and improvement opportunities visible in the source.

End-user features (reading, subscribing, the CLI's day-to-day use) are covered in the
companion **User Guide**. This document goes underneath that surface.

> [!NOTE]
> Everything here is derived from the repository source at the documented commit. Folo's
> production *backend API* (`api.folo.is`) is a separate service and is **not** part of
> this repository — the repo contains the clients (desktop, mobile, SSR, landing, CLI),
> the OTA update worker, and the shared packages they consume.

---

## 1. Executive Summary

**Folo** ("Follow everything in one place", formerly *Follow*) is an open-source,
AI-native information reader. The repository `RSSNext/Folo` is a large **pnpm + Turbo
monorepo** that produces multiple client applications from a shared core:

- **`apps/desktop`** — an Electron application. Its renderer (`@follow/web`) is the
  primary web app and is also what `app.folo.is` serves.
- **`apps/mobile`** — a React Native application built with Expo (iOS + Android, with
  native Swift/Kotlin modules).
- **`apps/ssr`** — a minimal server-side-rendering site for external sharing (feeds,
  lists, profiles, OG images), deployable to Cloudflare Workers / Vercel.
- **`apps/landing`** — the marketing site.
- **`apps/cli`** — the `folo` command-line client (npm package `folocli`).
- **`apps/ota`** — a Cloudflare Worker (Hono) that serves mobile over-the-air updates and
  version metadata.

These are unified by a fleet of shared packages under **`packages/internal/*`**
(components, atoms, hooks, store, database, shared, constants, models, tracker, utils,
logger, types) and a content-extraction package (`packages/readability`).

| Property | Value |
|----------|-------|
| Repository | `RSSNext/Folo` (analyzed at commit `c98f572`, 2026-06-05) |
| License | AGPL-3.0-only (Electron main process: GPL-3.0-only; `@follow/utils`: MIT) |
| Package manager | `pnpm@10.17.0` (Corepack-pinned) |
| Build orchestration | Turborepo (`turbo.json`) |
| Primary language | TypeScript (strict) |
| UI runtime | React 19 |
| Desktop shell | Electron 38 + electron-vite + electron-forge |
| Mobile runtime | Expo SDK 55 + React Native 0.83 |
| State | Jotai (atoms) · Zustand (stores) · TanStack Query (server state) |
| Local data | Drizzle ORM over SQLite (wa-sqlite/sqlocal on web, expo-sqlite on mobile) |
| Auth | better-auth (OAuth + email/password + 2FA), Stripe via `@better-auth/stripe` |

> [!IMPORTANT]
> The "API" the clients talk to (`https://api.folo.is`) is external to this repo. When
> running from source you point the clients at the production API. There is no bundled
> server to stand up for normal development.

---

## 2. Architecture

### 2.1 Big picture

```text
                         ┌─────────────────────────────────────────────┐
                         │            Folo Backend API                  │
                         │      https://api.folo.is  (external,         │
                         │      better-auth + feeds + AI + Power)       │
                         └───────────────▲──────────────▲──────────────┘
                                         │              │
            ┌────────────────────────────┼──────────────┼───────────────────────┐
            │                            │              │                        │
   ┌────────┴─────────┐        ┌─────────┴───────┐   ┌──┴──────────┐    ┌────────┴───────┐
   │  apps/desktop    │        │  apps/mobile    │   │  apps/cli   │    │   apps/ssr      │
   │  Electron        │        │  Expo / RN      │   │  folocli    │    │  share pages    │
   │  ┌────────────┐  │        │  + native iOS/  │   │  (commander │    │  + OG images    │
   │  │ main proc  │  │        │    Android mods │   │   + SDK)    │    │  (Cloudflare/   │
   │  │ (IPC svcs) │  │        └─────────────────┘   └─────────────┘    │   Vercel)       │
   │  ├────────────┤  │                                                 └─────────────────┘
   │  │ renderer = │  │        ┌─────────────────┐    ┌─────────────────┐
   │  │ @follow/web│◄─┼────────┤  apps/landing   │    │   apps/ota      │
   │  └────────────┘  │ shares │  marketing site │    │  mobile OTA     │
   └──────────────────┘  core  └─────────────────┘    │  (Hono worker)  │
            ▲                                          └─────────────────┘
            │ all consume
   ┌────────┴───────────────────────────────────────────────────────────────────┐
   │  packages/internal/*  — components · atoms(jotai) · hooks · store(zustand)   │
   │  · database(drizzle+sqlite) · shared(auth/env/stripe) · constants · models   │
   │  · tracker(analytics) · utils · logger · types      +  packages/readability  │
   └──────────────────────────────────────────────────────────────────────────────┘
```

The defining architectural idea is **one shared core, many surfaces**. Business logic,
data models, the store, and most UI primitives live in `packages/internal/*`. Each app is
a thin shell that wires the shared core into its platform (Electron, Expo, a worker, or a
terminal).

### 2.2 The desktop app: main / renderer split

`apps/desktop` follows the standard Electron two-process model, organized under
`layer/`:

- **`layer/main`** (`@follow/electron-main`, GPL-3.0) — the Node-privileged main process.
  It owns windows, the tray/dock, auto-update, the local key/value store
  (`electron-store`), proxy configuration, push notifications, and all native
  capabilities. It exposes them to the renderer through a typed IPC layer.
- **`layer/renderer`** (`@follow/web`) — the React 19 web app. This same renderer is what
  runs in the browser at `app.folo.is` (built with `WEB_BUILD=1`). When packaged in
  Electron it talks to the main process over IPC; in the browser those capabilities
  degrade gracefully or are unavailable.
- **`layer/main/preload`** — the preload bridge exposing the IPC surface to the renderer.

This is why the desktop renderer and the web app are the *same code*: the renderer is
written to run in a plain browser, and Electron simply adds native superpowers via IPC.

### 2.3 IPC architecture (desktop)

The main process exposes capabilities as **IPC services** using the
`electron-ipc-decorator` library. Each service is a class extending `IpcService` with a
static `groupName`, and each callable is a method decorated with `@IpcMethod()`. This
gives the renderer a typed, namespaced RPC surface (`<group>.<method>`).

| Service (group) | Responsibility (selected methods) |
|-----------------|-----------------------------------|
| **`app`** | Version, check/apply updates, open external URLs, window actions (close/minimize/maximize), clipboard, in-page find, file download, **export current page as PDF**, cache folder/limit/clear, reveal log file, select directory |
| **`auth`** | Session token management (set/clear/apply cookies), email sign-in/sign-up, **TOTP 2FA verify**, remote sign-out, sync session → CLI config |
| **`cli`** | CLI install status, connect/disconnect the `folo` CLI to the desktop session |
| **`setting`** | Login-item (launch at login), open settings window, system fonts, appearance (light/dark/system), minimize-to-tray, **proxy config**, messaging token |
| **`reader`** | **Readability** extraction, **TTS** (via `tts.folo.is`) + voice list, code-language detection |
| **`integration`** | Save to **Obsidian** (frontmatter Markdown), **Eagle** media save, **qBittorrent** login + add-magnet, custom fetch, open custom URL scheme |
| **`dock`** | Background **unread-count polling** → dock/taskbar badge |
| **`menu`** | Native context menus, confirm dialogs, native share menu |
| **`debug`** | Inspect element |

> [!TIP]
> To enumerate the entire renderer↔main privilege boundary, read
> `apps/desktop/layer/main/src/ipc/services/*.ts`. Every privileged operation the
> renderer can trigger is exactly one `@IpcMethod()` in one of these services — this is
> the desktop security review surface.

### 2.4 The mobile app

`apps/mobile` is an Expo (SDK 55) React Native 0.83 app. It shares the same store, models,
database services, and many components with desktop via `packages/internal/*`, but uses
native UI primitives:

- **Native modules** under `apps/mobile/native` (the `follow-native` Expo module) with
  Swift (iOS) and Kotlin (Android) code — tab bar, pager view, context menus, shared
  webview, indicators, store-kit test helpers.
- **NativeWind** (Tailwind for RN) + the React Native UIKit color system.
- **expo-sqlite** as the Drizzle driver (vs wa-sqlite on web).
- A **micro web-app** (`apps/mobile/web-app/html-renderer`) — a small Vite-built HTML
  renderer injected into a WebView to render rich entry content (shiki, katex,
  blurhash) consistently with the desktop renderer.
- Background refresh via `expo-background-task`, push via Firebase Messaging, audio via
  `react-native-track-player`.

### 2.5 SSR & sharing service

`apps/ssr` is a minimal SSR site for **external sharing** — public pages for feeds,
lists, user profiles, and OG (Open Graph) image generation. It is built with Vite +
React, served by **Fastify** locally and deployable as a **Cloudflare Worker** (and/or
Vercel — see `vercel.json`). OG images are rendered with **satori** + **resvg**.

Routes (from `apps/ssr/worker-entry.ts`):

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/healthz` | Health check |
| `GET` | `/feed/:id` | Public feed share page |
| `GET` | `/list/:id` | Public list share page |
| `GET` | `/profile/:path` | Public user profile |
| `GET` | `/og/:type/:id` | Generated OG image |
| `GET` | `/share/*` | Share routes |
| `GET` | `/login`, `/register`, `/forget-password`, `/reset-password` | Auth pages |
| `GET` | `*` | SPA fallback |

`xss` is used to sanitize rendered content — appropriate, since these pages render
third-party feed content to anonymous visitors.

### 2.6 OTA update service

`apps/ota` is a **Hono** Cloudflare Worker that powers **mobile over-the-air updates** and
the version badges shown in the README. It:

- Serves OTA release metadata and archives (with `fzstd`/`tar-stream` for the bundles).
- Aggregates store versions by querying the **Apple App Store**, **Google Play**, and the
  **Microsoft Store** (see `apps/ota/src/lib/store-version.ts`).
- Fetches GitHub releases for desktop version info.
- Validates OTA code signing (`apps/ota/src/lib/code-signing.ts`).

This is the service behind `ota.folo.is`.

### 2.7 The CLI

`apps/cli` (`folocli`) is a **commander**-based CLI that talks to the same API via the
`@follow-app/client-sdk`. It is intentionally small and dependency-light:

- `src/index.ts` registers ten command groups (auth, timeline, subscription, entry, feed,
  list, search, collection, opml, unread) and global options.
- `src/client.ts` builds a `FollowClient` with the resolved token and base URL.
- `src/config.ts` persists `{ token, apiUrl }` to `~/.folo/config.json`.
- `src/browser-login.ts` implements `folo login`: it spins up a local `127.0.0.1`
  callback HTTP server, opens the browser to the web login page with a `cli_callback`
  param, receives a one-time token, and exchanges it (`/better-auth/one-time-token/apply`,
  falling back to `/verify`, then a session probe) for a session token.
- `src/output.ts` renders the `{ok,data,error}` envelope as JSON, an ASCII table, or
  plain text.

The desktop app's `auth` IPC service syncs the desktop session into the CLI config so the
two share a sign-in.

### 2.8 State management

Three complementary tools, by design (see root `AGENTS.md`):

| Tool | Used for |
|------|----------|
| **Jotai** | Fine-grained atoms — settings, UI state. Centralized in `packages/internal/atoms`. |
| **Zustand** | Larger domain stores — the data model in `packages/internal/store` (entry, feed, subscription, list, collection, unread, summary, translation, user, inbox, image, action). |
| **TanStack Query** | Server state — fetching/caching API data, with persisted query cache. |

### 2.9 Data layer

`packages/internal/database` is a **Drizzle ORM** schema over SQLite, with a
platform-specific driver:

- **Web/desktop renderer:** `wa-sqlite` / `sqlocal` (SQLite compiled to WASM, persisted
  via OPFS/IndexedDB).
- **Mobile:** `expo-sqlite`.

Schemas live in `src/schemas/`; migrations are generated with `drizzle-kit generate` and
applied by a custom migrator (`src/migrator.ts`). A `ResourceLock` guards concurrent
access. Above the raw tables sits a **service layer** (`src/services/*`) of static service
objects — `EntryService`, `FeedService`, `SubscriptionService`, `CollectionService`,
`ListService`, `InboxService`, `ImageService`, `SummaryService`, `TranslationService`,
`UnreadService`, `UserService` — that the Zustand stores call. The `@follow/store`
package's `morph/` and `hydrate.ts` translate API payloads into the local schema.

---

## 3. Repository Structure

```text
RSSNext/Folo
├── apps/
│   ├── desktop/                 # Electron app
│   │   ├── layer/
│   │   │   ├── main/            # @follow/electron-main (Node main process)
│   │   │   │   ├── src/ipc/services/   # IPC services (app/auth/cli/...)
│   │   │   │   ├── src/lib/            # store, tray, dock, proxy, updater, auth-cookies
│   │   │   │   ├── src/manager/        # app/window managers
│   │   │   │   ├── src/updater/        # auto-update + hot-update (dynamic render entry)
│   │   │   │   └── preload/            # IPC bridge
│   │   │   └── renderer/        # @follow/web (React 19 renderer = the web app)
│   │   │       └── src/modules/        # feature modules (ai-chat, entry-*, settings, ...)
│   │   ├── e2e/                 # Playwright (electron + web)
│   │   └── plugins/vite/        # build plugins (main-hash, route builder)
│   ├── mobile/                  # @follow/mobile (Expo / React Native)
│   │   ├── native/              # follow-native Expo module (Swift / Kotlin)
│   │   ├── web-app/html-renderer/  # WebView content renderer
│   │   └── src/modules/         # feature modules (entry-list, settings, ai, ...)
│   ├── ssr/                     # @follow/ssr (share pages + OG images)
│   ├── landing/                 # @follow/landing (marketing, vinext + Cloudflare)
│   ├── cli/                     # folocli (commander)
│   └── ota/                     # @follow/ota (Hono Cloudflare Worker)
├── packages/
│   ├── internal/
│   │   ├── components/          # @follow/components (shared UI: radix, lexical, ...)
│   │   ├── atoms/               # @follow/atoms (jotai)
│   │   ├── hooks/               # @follow/hooks
│   │   ├── store/               # @follow/store (zustand domain stores)
│   │   ├── database/            # @follow/database (drizzle + sqlite)
│   │   ├── shared/              # @follow/shared (auth, env, stripe, bridge)
│   │   ├── constants/           # @follow/constants (views, roles, rsshub, ...)
│   │   ├── models/              # @follow/models
│   │   ├── tracker/             # @follow/tracker (posthog/firebase/proxy adapters)
│   │   ├── utils/               # @follow/utils (MIT; readability, color, codecs, ...)
│   │   ├── logger/              # @follow/logger (electron-log / web)
│   │   └── types/               # @follow/types (global d.ts)
│   ├── configs/                 # @follow/configs (tailwind, tsconfig presets)
│   └── readability/             # @follow-app/readability (Mozilla Readability)
├── locales/                     # i18n JSON (app/ai/settings/errors/shortcuts/native/...)
├── icons/                       # icon assets (icons/mgc is proprietary MingCute)
├── api/                         # vercel webhook (e.g. Cloudflare cache purge)
├── docs/                        # docs/superpowers (plans/specs)
├── scripts/                     # repo scripts (icons, proxy, ota build)
├── turbo.json  pnpm-workspace.yaml  vercel.json  eslint.config.mjs
└── README.md  AGENTS.md  CONTRIBUTING.md  SECURITY.md
```

> [!NOTE]
> Each app/package may carry its own `AGENTS.md` with rules that override the root one for
> files in that subtree (e.g. `apps/desktop/AGENTS.md`, `apps/mobile/AGENTS.md`,
> `packages/internal/AGENTS.md`). The closest guide to the edited file wins.

---

## 4. Shared Internal Packages

| Package | Purpose | Key dependencies |
|---------|---------|------------------|
| `@follow/components` | Shared React UI primitives & modules | Radix UI, Lexical (rich editor), unified/rehype/remark (Markdown), KaTeX, motion, vaul |
| `@follow/atoms` | Jotai atoms (settings, UI state) | jotai |
| `@follow/hooks` | Reusable React hooks | TanStack Query, foxact, usehooks-ts |
| `@follow/store` | Zustand domain stores + morph/hydrate | zustand, immer, TanStack Query, batshit |
| `@follow/database` | Drizzle schema, services, migrator | drizzle-orm, wa-sqlite/sqlocal, expo-sqlite, `ai` |
| `@follow/shared` | Auth, env (`@t3-oss/env-core`), Stripe, IPC bridge | better-auth, `@better-auth/stripe`, stripe, drizzle, zod, `ai` |
| `@follow/constants` | Enums (views, roles, routes), RSSHub categories, social links, store IDs | client-sdk |
| `@follow/models` | Domain models / types (incl. RSSHub) | client-sdk |
| `@follow/tracker` | Analytics with pluggable adapters | posthog, firebase analytics |
| `@follow/utils` (MIT) | Pure utilities: readability, color, JSON codecs, LRU cache, URL builder, path parser, sanitization | Mozilla Readability, dompurify, linkedom, hast/rehype |
| `@follow/logger` | Logging (electron-log on desktop, web logger elsewhere) | electron-log |
| `@follow/configs` | Tailwind preset (Apple UIKit colors), tsconfig presets, icon tooling | tailwindcss plugins, iconify |
| `@follow-app/readability` | Standalone content extraction | `@mozilla/readability`, linkedom, dompurify, chardet |

> [!TIP]
> `@follow/utils` is MIT-licensed (the rest of the app is AGPL/GPL). If you want to reuse
> Folo's utility code in a non-AGPL project, this is the package that permits it.

### Design tokens & UI conventions

The UI is built on **Tailwind bound to Apple UIKit color tokens** (light/dark adaptive),
prefixed by CSS property (`text-*`, `bg-*`, `border-*`, plus material/control/interface
groups). Icons use **MingCute** via the `i-mgc-` class prefix. Motion uses **Framer
Motion** via `m.*` (LazyMotion) with spring presets, but the team standard is to prefer
**CSS transitions** for simple interactions to avoid frame drops. On mobile, NativeWind +
the RN UIKit color system play the same role. See root `AGENTS.md` for the full token map.

---

## 5. Authentication & Identity

Folo uses **better-auth** end to end. Salient facts for operators and integrators:

- **Methods:** Google / GitHub / Apple OAuth, email + password, and **TOTP 2FA**.
- **Session transport:** a session-token cookie. The cookie name is derived per API URL
  (e.g. `__Secure-better-auth.session_token` on HTTPS). The desktop main process manages
  these cookies in the Electron session (`auth.ts` IPC service:
  `applySessionToken`/`clearSessionToken`, dedupe, 30-day expiry).
- **Desktop auth flow:** email sign-in/up POSTs to `/better-auth/sign-in/email` and
  `/sign-up/email`; 2FA via `/better-auth/two-factor/verify-totp`; sign-out hits
  `/better-auth/sign-out`. Origin/desktop headers are attached on every request.
- **CLI auth flow:** browser login exchanges a one-time token at
  `/better-auth/one-time-token/apply` (fallback `/verify`), validated against
  `/better-auth/get-session`. Token stored in `~/.folo/config.json`.
- **Roles & billing:** `UserRole` (Free/Basic/Plus/Pro/Admin) gates limits; **Stripe** is
  integrated via `@better-auth/stripe` (`packages/internal/shared`).

The session-token-as-cookie design is what enables the "paste your browser cookie into the
desktop app" fallback documented in the User Guide.

---

## 6. Configuration

There are **two distinct configuration worlds**. Conflating them is the most common
mistake when reasoning about Folo's settings.

### 6.1 User-facing application settings

End-user settings (appearance, reading behavior, AI, actions, integrations) are **not**
files or environment variables. They are stored:

- **Renderer/web:** Jotai atoms + browser persistence (IndexedDB/localStorage).
- **Desktop main:** `electron-store` (`store.ts`) for native-side settings — appearance,
  minimize-to-tray, proxy config, cache size limit, login-item, messaging token.
- **Mobile:** Jotai atoms + a settings **sync queue** (`SettingSyncQueue`) that reconciles
  settings to the account.

These sync with the account so settings follow the user across devices. There is no
config file to hand-edit for normal operation.

### 6.2 Build / runtime environment variables

Environment variables in this repo are for **building and CI**, not app settings. The full
set found in source:

| Variable | Where | Purpose |
|----------|-------|---------|
| `WEB_BUILD` | `apps/desktop/vite.config.ts` | Build the renderer as the standalone web app |
| `SSL` | desktop vite config | Enable HTTPS dev server (mkcert) |
| `analyzer` | desktop vite config | Bundle analysis build |
| `FOLO_E2E_USER_DATA_DIR` | desktop `before-bootstrap.ts` | E2E isolated user-data dir |
| `PROFILE`, `NODE_ENV` | mobile `app.config.base.ts` | Build profile / mode |
| `OTA_RUNTIME_VERSION` | mobile config | OTA runtime version pin |
| `EXPO_PUBLIC_APP_CHECK_DEBUG_TOKEN` | mobile env | Firebase App Check debug token (dev) |
| `EXPO_PUBLIC_E2E_ENV_PROFILE`, `EXPO_PUBLIC_E2E_LANGUAGE` | mobile | E2E config |
| `VERCEL` | `apps/ssr/index.ts` | Detect Vercel runtime |
| `VITE_API_URL`, `VITE_WEB_URL` | desktop `.env` | Point clients at the API/web origin |
| `CI`, `DEBUG`, `VITEST` | various | Tooling flags |
| `FOLO_TOKEN`, `FOLO_TEST_TOKEN` | CLI | CLI session token / test token |

The canonical API/web origins per environment (prod/dev/local) live in
`packages/internal/shared/src/env.common.ts` (`DEFAULT_VALUES`), which the CLI's browser
login and the apps both read.

> [!IMPORTANT]
> For local development you copy `apps/desktop/.env.example` to `.env` and set
> `VITE_API_URL=https://api.follow.is` (per `CONTRIBUTING.md`). The mobile app needs an
> `EXPO_PUBLIC_APP_CHECK_DEBUG_TOKEN` in its `.env`.

---

## 7. Development Setup

### Prerequisites

- **Node.js** (see `.nvmrc`) and **Corepack** enabled (`corepack enable && corepack prepare`).
- **pnpm 10.17.0** (pinned via `packageManager`).
- For mobile: a **Mac with Xcode** (iOS) and/or Android toolchain; Expo dev client.

### Install

```bash
corepack enable && corepack prepare
pnpm install        # postinstall builds the internal packages (build:packages)
```

### Run each surface

```bash
# Desktop — browser renderer (recommended for most dev)
cd apps/desktop && pnpm run dev:web
#   opens https://app.folo.is/__debug_proxy against the online API

# Desktop — full Electron
cd apps/desktop
cp .env.example .env            # set VITE_API_URL=https://api.follow.is
pnpm run dev:electron

# Mobile (Expo) — requires Mac/Xcode for iOS
cd apps/mobile
cp .env.example .env            # set EXPO_PUBLIC_APP_CHECK_DEBUG_TOKEN
pnpm run ios                    # build & install dev client (first run only)
pnpm run dev

# SSR share site
cd apps/ssr && pnpm run dev

# Landing
cd apps/landing && pnpm run dev

# CLI (from source)
cd apps/cli && pnpm run dev -- --help
```

### Quality gates (must pass before commit/PR)

The order is enforced: **typecheck → lint → test**.

```bash
pnpm run typecheck          # turbo typecheck across packages
pnpm run lint:fix           # tsslint + eslint --fix
pnpm run test               # cross-env CI=1, recursive vitest

# Or via turbo:
pnpm exec turbo run format:check typecheck lint
pnpm exec turbo run test
```

A `simple-git-hooks` pre-commit runs `lint-staged` (eslint + prettier; locale dedupe;
mobile build-id increment).

> [!TIP]
> Co-locate Vitest tests next to source. After moving files or changing imports, run
> `pnpm lint` and `pnpm typecheck` for the affected package before pushing — CI requires
> all three gates green.

---

## 8. Build & Deployment

### 8.1 Desktop (Electron)

Built with **electron-vite** (bundling) + **electron-forge** (packaging). Makers are
configured for every platform:

| Target | Maker |
|--------|-------|
| macOS `.dmg` / `.pkg` | `maker-dmg`, `maker-pkg` |
| macOS App Store (MAS) | `--platform=mas` universal |
| Windows installer | `maker-squirrel` |
| Windows Store (APPX) | `maker-appx` (manifest generated by a script) |
| Linux AppImage | `electron-forge-maker-appimage` |
| Cross-platform zip | `maker-zip` |

```bash
cd apps/desktop
pnpm run build:electron                # vite build + forge make
pnpm run build:web                     # WEB_BUILD=1 vite build → out/web (the web app)
pnpm run publish                       # build + forge publish (GitHub publisher)
```

Auto-update uses `electron-updater`; there's also a **hot-update** path
(`updater/hot-updater.ts`, `app.rendererUpdateReload` IPC + a `mainHash` in
`package.json`) that can swap the renderer bundle without a full reinstall.

> [!CAUTION]
> The desktop `package.json` carries a `mainHash` integrity value tied to the main-process
> bundle. Regenerate it (`pnpm run update:main-hash`) when the main bundle changes;
> otherwise hot-update integrity checks will reject the build.

### 8.2 Web app

The web app is the desktop renderer built with `WEB_BUILD=1` (`pnpm run build:web` →
`out/web`). This is what `app.folo.is` serves. It's a Vite build with `vite-plugin-pwa`
(installable PWA, service worker) and legacy support.

### 8.3 SSR & landing (Cloudflare / Vercel)

- **`apps/ssr`** builds with Vite + `tsdown` and deploys via **Wrangler**
  (`deploy:dev`/`deploy:prod`) and/or Vercel (`vercel.json` + a prepare/cleanup script).
- **`apps/landing`** uses **vinext** + the Cloudflare Vite plugin; `cf:deploy` ships it to
  Cloudflare.

### 8.4 Mobile (Expo / EAS)

```bash
cd apps/mobile
pnpm run ios | pnpm run android        # local dev builds
pnpm run update:export                 # expo export (iOS+Android bundles)
pnpm run update:bundle                 # build OTA release bundle
```

Native builds go through **EAS** (`eas-cli`). OTA updates are produced here and served by
`apps/ota`.

### 8.5 OTA worker

```bash
cd apps/ota
pnpm run deploy:dev | pnpm run deploy:prod   # wrangler deploy
```

### 8.6 CLI

```bash
cd apps/cli
pnpm run build                         # tsup → dist/index.js (bin: folo)
# published to npm as folocli
```

### 8.7 Cache-purge webhook

`api/vercel_webhook.ts` is a small serverless webhook that (given a Cloudflare token and
zone ID) purges CDN cache — used in the deploy pipeline. Missing credentials throw
`No Cloudflare token or zone ID found`.

---

## 9. Security

Folo is a client application that talks to an external API, renders untrusted third-party
content, and (on desktop) bridges to native capabilities. The security surface follows
from that.

### 9.1 Untrusted content rendering

Feeds and entries are attacker-influenced HTML. Folo sanitizes aggressively:

- `@follow/utils` and `@follow/components` use **rehype-sanitize** / **hast-util-sanitize**
  and **DOMPurify** before rendering Markdown/HTML.
- The SSR share pages use **`xss`** to sanitize content rendered to anonymous visitors.
- Reader-mode extraction (`@follow-app/readability`) runs in a controlled DOM (`linkedom`)
  rather than the live page.

> [!WARNING]
> Any new code path that injects feed-derived HTML (e.g. a custom renderer or a new
> integration that emits markup) must route through the existing sanitizers. The
> renderer renders feed content verbatim by design — sanitization is the only thing
> standing between a malicious feed and the user.

### 9.2 Desktop privilege boundary (IPC)

The renderer is browser-grade untrusted relative to the main process. Every privileged
operation is a single `@IpcMethod()` (see §2.3). Notable hardening already present:

- The `integration` service **masks** sensitive query params and headers
  (`token`/`key`/`password`/`authorization`) in logs.
- `openURLScheme` validates the scheme format before calling `shell.openExternal`.
- Obsidian filenames are sanitized (`INVALID_CHAR_REGEX`, drive-letter handling) to
  prevent path traversal / NTFS alternate-data-stream tricks.
- `customFetch` enforces a timeout and aborts.

> [!CAUTION]
> `customFetch` and `openURLScheme` are powerful primitives reachable from the renderer.
> Treat them as a trust boundary: validate inputs server-of-the-bridge-side, and never
> widen them to accept renderer-controlled headers without masking/allow-listing. A
> compromised or malicious feed that can drive renderer code could otherwise use these as
> an SSRF / arbitrary-request primitive.

### 9.3 Local integration credentials

The desktop app stores third-party credentials in `electron-store`:

- **qBittorrent** SID cookie (`qbittorrentSID`) after login with host/username/password.
- **Eagle** talks to `localhost:41595` (assumes a local trusted Eagle instance).

These are local-trust integrations; document to users that enabling them grants Folo the
ability to act against those local services.

### 9.4 Network & proxy

The desktop app supports a user-configured proxy (`setting.setProxyConfig`). Requests use
`ofetch`/`ky`/`fetch` with desktop/origin headers. TTS goes to `tts.folo.is`; readability
fetches the original article URL (an outbound request to arbitrary user-chosen origins —
expected for a reader).

### 9.5 Mobile

- Secrets via `expo-secure-store`; Firebase **App Check** gates API abuse.
- Native modules run with app privileges; the injected WebView renderer should only
  receive sanitized content.

### 9.6 Supply chain & provenance

- Releases are signed (SignPath on Windows; Apple notarization on macOS/iOS) and carry
  **GitHub artifact attestations**. The OTA worker validates code signing on update
  bundles.
- A few dependencies are pinned to **forks / git URLs** (`wa-sqlite` from a git tag, a
  forked `sqlocal`, a forked `react-resizable-layout`). Track these for upstream security
  fixes since they bypass the normal registry update path.

### 9.7 Reporting

Per `SECURITY.md`, report vulnerabilities to **[email removed]**. Always run the latest
version for security updates.

---

## 10. Analytics, AI & Notifications

### Analytics (`@follow/tracker`)

A small tracker abstraction with pluggable **adapters**: PostHog (web + React Native),
Firebase Analytics (mobile), and a Proxy adapter. `TrackManager` extends `TrackerManager`;
events route through registered adapters. This makes it straightforward to add/remove
analytics backends or disable them.

### AI (`ai` SDK)

The Vercel **`ai`** SDK (v6) powers summary, translation, and the **AI Chat** module
(`apps/desktop/layer/renderer/src/modules/ai-chat`), which includes a Lexical-based rich
editor with plugins (mentions, file upload, shortcuts), a chat-core store, and an
event system. **BYOK** lets users supply their own provider key (Settings → AI). AI
prompt/locale strings live under `locales/ai`.

### Notifications

- **Desktop:** push receiver (`@eneris/push-receiver`) + a messaging token; the `dock`
  service polls unread counts in the background to update the badge.
- **Mobile:** Firebase Cloud Messaging (`@react-native-firebase/messaging`) +
  `expo-notifications`.

---

## 11. Internationalization

i18n uses **i18next** with **flat keys only** (no `defaultValue`, no conflicting dotted
keys). Locale JSON is centralized under top-level `locales/` and split by domain:
`app`, `ai`, `settings`, `errors`, `shortcuts`, `native`, `common`, `lang`, `mobile`. Each
feature should provide at least `en`, `zh-CN`, and `ja`. A repo script (`dedupe:locales`)
and a lint-staged hook keep locale files deduplicated.

> [!TIP]
> The `locales/` tree is large (settings alone is hundreds of KB). When adding a feature,
> add its keys to the relevant domain file rather than creating a new top-level namespace,
> and run `pnpm run dedupe:locales`.

---

## 12. Operations & Troubleshooting (Developer)

### Reproducing user-reported issues

The team triages issues on GitHub. Recurring categories and where to look:

| Symptom | Likely area |
|---------|-------------|
| TTS not working | `reader` IPC service + `tts.folo.is` availability |
| Auto-translation blanks content | renderer translation + reader-mode interaction |
| High CPU / overheating (Apple Silicon, mobile) | media-heavy view rendering, animations (prefer CSS transitions) |
| Mobile login failures | better-auth OAuth/redirect handling, App Check |
| Captcha failures | recaptcha (`react-google-recaptcha-v3`) on sensitive flows |
| Embedded video anti-bot prompt | upstream site blocking the webview; offer external-open |
| Inbox email not received | account inbox routing (backend) |

### Logs & diagnostics

- Desktop: `electron-log`; **Settings → Data & cache → Reveal log file**, or the
  `app.revealLogFile` IPC method.
- CLI: run with `--verbose` to log every request/response to stderr.
- Renderer: `react-scan` and `why-did-you-render` are wired for perf debugging in dev.

### E2E

- Desktop: **Playwright** (`e2e/playwright.config.ts`, projects `electron` and `web`).
- Mobile: **Maestro** flows (`apps/mobile/e2e`, shared/iOS/Android), run via
  `pnpm run e2e:ios` / `e2e:android`.

---

## 13. Code Quality & Conventions

From `AGENTS.md` and the lint config:

- **TypeScript strict**, no `any`; precise types. English comments.
- **Imports:** use `pathe` instead of `node:path` for cross-platform paths.
- **Styling:** avoid inline styles in JSX; extract complex styles to a `styles.ts`
  alongside the component (typed with `CSSProperties`). On mobile, use NativeWind, not
  `StyleSheet.create`, for new UI.
- **Components:** shared/reusable UI goes in `packages/internal/components` and is exported
  from the package index; app-specific UI stays in the app
  (`.../modules/renderer/components` on desktop).
- **Motion:** CSS transitions first; Framer Motion `m.*` only when necessary.
- **Tooling:** ESLint 9 (`eslint-config-hyoban`) + **tsslint** (type-aware lint), Prettier
  with the Tailwind plugin, Turbo for orchestration.

The command-palette system (`apps/desktop/.../modules/command`) is modeled on VS Code's
command contributions, with a registry adapted from AFFiNE — a clean pattern to follow
when adding new commands (register once, surface in palette + context menus).

---

## 14. Potential Improvements & Technical Debt

Observations from the source that maintainers may want to prioritize. None are defects per
se; they are areas where investment would pay off.

- **Reader-mode + auto-translation interaction** (reported): content can disappear when
  auto-translation is on. Worth an integration test that renders a translated entry and
  asserts the body is present.
- **TTS resilience:** TTS depends on a single external service (`tts.folo.is`). Add
  graceful degradation/caching and clearer UI when the service is unavailable (cache
  already exists per-utterance; surface failures better).
- **Forked/git dependencies:** `wa-sqlite` (git tag), `sqlocal` fork, `react-resizable-layout`
  fork. These bypass registry updates — track upstream for security/bug fixes and plan
  to converge on released versions.
- **`customFetch` / `openURLScheme` blast radius:** powerful renderer-reachable primitives.
  Consider an allow-list of hosts/schemes and stronger input validation to reduce SSRF
  potential (see §9.2).
- **Performance on media-heavy views:** overheating/high-CPU reports cluster around
  Pictures/Videos grids and animations. Continue the "CSS transitions over JS motion"
  push; consider virtualization audits and content-visibility tuning (the
  `tailwindcss-content-visibility` plugin is already present).
- **Mobile auth robustness:** OAuth/redirect login failures on mobile recur; harden the
  callback handling and surface actionable errors.
- **Two render engines for entry content:** the desktop renderer and the mobile WebView
  `html-renderer` both render rich content (shiki/katex/blurhash). Keep them in lockstep to
  avoid divergence in how an entry looks across platforms.
- **Documentation gap** (reported #4903/#5000): users ask for an official user guide and
  docs on writing automation/notification regular expressions. A short docs site for
  Actions patterns would cut support load.

---

## 15. Quick Reference

### Apps & their package names

| Path | Package | Runtime / deploy |
|------|---------|------------------|
| `apps/desktop` | `Folo` (renderer `@follow/web`, main `@follow/electron-main`) | Electron / electron-forge; web → `out/web` |
| `apps/mobile` | `@follow/mobile` | Expo / EAS (iOS, Android) |
| `apps/ssr` | `@follow/ssr` | Fastify / Cloudflare Worker / Vercel |
| `apps/landing` | `@follow/landing` | vinext / Cloudflare |
| `apps/cli` | `folocli` (bin `folo`) | npm package, Node ≥ 18 |
| `apps/ota` | `@follow/ota` | Hono / Cloudflare Worker |

### Common commands

```bash
pnpm install                         # install + build internal packages
pnpm run typecheck                   # turbo typecheck
pnpm run lint:fix                    # tsslint + eslint --fix
pnpm run test                        # recursive vitest (CI=1)
pnpm run build:web                   # build the web app (desktop renderer)
pnpm exec turbo run format:check typecheck lint
```

### Per-app dev

```bash
cd apps/desktop && pnpm run dev:web        # web renderer against online API
cd apps/desktop && pnpm run dev:electron   # full Electron (needs .env)
cd apps/mobile  && pnpm run dev            # Expo
cd apps/ssr     && pnpm run dev            # SSR
cd apps/cli     && pnpm run dev -- --help  # CLI
```

### Key external endpoints

| Endpoint | Role |
|----------|------|
| `https://api.folo.is` (a.k.a. `api.follow.is`) | Backend API + better-auth (external) |
| `https://app.folo.is` | Hosted web app (the desktop renderer) |
| `https://ota.folo.is` | OTA updates + version metadata (`apps/ota`) |
| `https://tts.folo.is` | Text-to-speech service (desktop `reader` IPC) |
| `localhost:41595` | Local Eagle app (integration) |

### Auth endpoints (better-auth)

| Path | Use |
|------|-----|
| `/better-auth/sign-in/email`, `/sign-up/email` | Email auth (desktop) |
| `/better-auth/two-factor/verify-totp` | 2FA |
| `/better-auth/sign-out` | Sign out |
| `/better-auth/one-time-token/apply` (`/verify`) | CLI token exchange |
| `/better-auth/get-session` | Session validation |

### CLI config

```text
~/.folo/config.json     →  { "token": "...", "apiUrl": "https://api.folo.is" }
FOLO_TOKEN              →  env override for the token
```

---

_Generated June 2026 from RSSNext/Folo at c98f572. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
