Jesse A. Green / notebook

Folo Admin & Developer Guide

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.

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

                         ┌─────────────────────────────────────────────┐
                         │            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

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

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

Run each surface

# 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.

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
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)

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

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

8.6 CLI

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

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

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

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