A light, highly-functional Pleroma, Akkoma and Mastodon compatible frontend.
  • TypeScript 95.4%
  • CSS 4%
  • JavaScript 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
fediverse.org c4ec50555f
Some checks are pending
CI / test-and-budget (push) Waiting to run
initial commit
2026-08-04 15:19:52 +09:00
.github/workflows initial commit 2026-08-04 15:19:52 +09:00
assets initial commit 2026-08-04 15:19:52 +09:00
public initial commit 2026-08-04 15:19:52 +09:00
scripts initial commit 2026-08-04 15:19:52 +09:00
src initial commit 2026-08-04 15:19:52 +09:00
test initial commit 2026-08-04 15:19:52 +09:00
.gitignore initial commit 2026-08-04 15:19:52 +09:00
AGENTS.md initial commit 2026-08-04 15:19:52 +09:00
DESIGN.md initial commit 2026-08-04 15:19:52 +09:00
eslint.config.mjs initial commit 2026-08-04 15:19:52 +09:00
FILTERING.md initial commit 2026-08-04 15:19:52 +09:00
index.html initial commit 2026-08-04 15:19:52 +09:00
lighthouserc.json initial commit 2026-08-04 15:19:52 +09:00
package-lock.json initial commit 2026-08-04 15:19:52 +09:00
package.json initial commit 2026-08-04 15:19:52 +09:00
README.md initial commit 2026-08-04 15:19:52 +09:00
TODO.md initial commit 2026-08-04 15:19:52 +09:00
tsconfig.json initial commit 2026-08-04 15:19:52 +09:00
vite.config.ts initial commit 2026-08-04 15:19:52 +09:00
vitest.config.ts initial commit 2026-08-04 15:19:52 +09:00

lightfe

A mobile-first fediverse PWA (Pleroma/Mastodon) built with Solid, Vite and TypeScript. Old-light-Twitter look, modern-X layout. The full architecture is specified in DESIGN.md — read it before changing anything structural.

Status: phases 13 complete

The read/resident/live release is complete: OAuth, capability detection, account verification, resident feeds, transient routes, filtering, persistence and live updates run through the worker-first architecture. Deterministic fixtures remain for isolated tests.

Implemented:

  • Design system: single hand-written src/styles.css (tokens, cascade layers, one light theme); feature expansion is temporarily allowed above the 4 KB compressed release budget while the CSS remains deliberately lean.
  • All components: Status (timeline + thread-root variants, CWs via <details>, boosts, polls, Pleroma emoji reactions, visibility icons), Timeline (pending pill, empty/error/loading states), Media (grid, blurhash DC-term placeholders, ALT badges), Composer, TabBar, Header, TabStrip, NotificationItem.
  • Routes: Home/Local/Federated/Notifications (Tier A, resident, stacked with content-visibility: hidden), Thread/Profile/Search/Compose and device-local Filters settings (Tier B).
  • Hand-written router, bounded timeline ring buffer with eviction/idle resident trimming, opt-in automatic live insertion, account and status maps, and a registry with Tier B LRU.
  • Live data-worker API client and normalization for home, local, federated and notification feeds; render-ready pages enter module-scope Solid stores directly.
  • OAuth app registration, PKCE/plain authorization, callback exchange, multi-account session persistence, refresh/revoke, capability detection and verified identity hydration.
  • One multiplexed WebSocket for all resident feeds, with worker-side event parsing, ordered store updates, background grace/reconnect catch-up and active-feed-only polling fallback.
  • Account-isolated IndexedDB snapshots written and read in the data worker; cold cache hydration races live requests without stale-overwrite risk; catch-up and streamed create/edit/delete events incrementally maintain a bounded, pruned offline head.
  • Advanced client-side filters with 11 composable rule types, safe cached regexes, per-instance IndexedDB persistence, hide/collapse presentation and worker-side application to pages, streams, notifications and retained rows.
  • Centralized custom emoji substitution with a cached instance map and ephemeral remote-entity fallbacks, safe normalized links, and mention/tag fallbacks for nonstandard servers.
  • Real worker-backed thread context and profile Posts/Replies/Media timelines, held in the shared Tier B LRU and lazy-loaded off the initial route.
  • Versioned service-worker shell precache, capped avatar/emoji stale-while-revalidate, explicit update activation, offline banner and labelled disabled actions.
  • Deferred worker-backed favourite/boost mutations with optimistic rollback, plus reply context and platform sharing.
  • Deferred idempotent text/CW/reply posting, a capability-aware visibility picker, capability-limited polls, and debounced per-account drafts in worker-owned IndexedDB.
  • Dedicated lazy media processing with transferable bytes, off-thread image downscaling, ALT text, compatible v2/v1 upload, and 30-second idle teardown.
  • Deferred, debounced /api/v2/search across normalized people, hashtags and statuses, with federated resolution and stale-response exclusion.
  • Accessible multi-account switching with synchronous cross-account data isolation, immediate account-scoped cache hydration and race-safe capability and live-transport activation.
  • Private bookmarks and owned lists with optimistic bookmark toggles, worker-normalized filtered timelines and Link-header pagination.
  • Capability-gated Pleroma/Akkoma emoji reaction toggles, server-provided translations, and mounted-only streaming for hashtag and list timelines.
  • One lazy compose/reaction emoji picker with a worker-owned, paged custom catalog, Unicode 17 search metadata and fixed-row DOM virtualization.
  • Capability-gated Akkoma bubble timeline with worker-side normalization, advanced filtering, bounded Tier B caching and Link-header pagination.

Commands

npm run dev        # dev server
npm run lint       # type-aware TypeScript/Solid/JSDoc lint
npm test           # vitest + jsdom — components render from fixtures, no server
npm run build      # typecheck + production build
npm run verify     # lint, coverage, type/build and Brotli budget assertions

Deployment instance policy

Deployments may serve a runtime policy from /.well-known/lightfe-config.json. The file is deployment configuration and is deliberately not included in the build, so the same exact dist/ artifacts can be used for unrestricted and server-locked installations. If it is missing, unreachable, or invalid, Lightfe always starts with its built-in unrestricted policy, default branding, and all locale catalogs bundled with that build.

For an unrestricted installation, serve:

{"mode":"open"}

For a deployment locked to one Fediverse server, serve:

{
  "mode": "locked",
  "allowedHosts": ["shitposter.world"],
  "defaultHost": "shitposter.world",
  "allowAdditionalAccounts": true,
  "branding": {
    "name": "Shitposter World",
    "loginHtml": "<p>Welcome to <strong>Shitposter World</strong>.</p>",
    "iconUrl": "/branding/shitposter-world.svg"
  },
  "localization": {
    "defaultLocale": "en-US",
    "locales": [
      {
        "code": "en-US",
        "label": "English (US)",
        "url": "/locales/en-US.json"
      },
      {
        "code": "de-DE",
        "label": "Deutsch",
        "url": "/locales/de-DE.json"
      },
      {
        "code": "es-419",
        "label": "Español (Latinoamérica)",
        "url": "/locales/es-419.json"
      },
      {
        "code": "ja-JP",
        "label": "日本語",
        "url": "/locales/ja-JP.json"
      }
    ]
  }
}

The entire branding object and each of its fields are optional:

  • name replaces the user-facing lightfe name, browser title, and installed PWA name. It must contain 180 characters. It does not change the OAuth client name or internal storage keys.
  • loginHtml adds optional rich copy above the instance control in the sign-in card. It accepts at most 4,096 characters and is sanitized through the same strict allowlist as federated post HTML; scripts, forms, inline styles, event handlers, and other unsafe markup are removed. When omitted, Lightfe displays “A mobile-first, lightweight Pleroma and Mastodon compatible frontend.” Set it to an empty string to hide the copy.
  • iconUrl replaces the sign-in-page mark. It must be a root-relative path or an absolute HTTPS URL; a failed image falls back to the built-in mark.

Localization

The build includes public/locales/en-US.json, public/locales/de-DE.json, public/locales/es-419.json, and public/locales/ja-JP.json, containing US English, idiomatic German, Latin American Spanish, and Japanese interface copy. A locale catalog has this shape:

{
  "locale": "en-US",
  "direction": "ltr",
  "messages": {
    "nav.home": "Home",
    "login.signInWith": "Sign in with {host}"
  }
}

To add a locale without making a separate application build:

  1. Translate a copy of public/locales/en-US.json, retaining every message key and named placeholder.
  2. Place it under the deployment web root, such as /locales/ja-JP.json.
  3. Add its {code, label, url} entry to localization.locales in /.well-known/lightfe-config.json and optionally make it defaultLocale.

code must be a valid BCP 47 language tag, label should be the language's native display name, and url must be a same-origin root-relative path. At most 32 locales may be advertised. Only advertised locales appear in the right-aligned login-page selector, and only the selected catalog is loaded. The selection is saved for one year in the independent lightfe_locale cookie. Catalog JSON is parsed and validated in the data worker. Set locale responses to Content-Type: application/json. The client revalidates the selected catalog at startup, so deployments may update an unversioned catalog without leaving a newer interface paired with stale text. Versioned URLs remain appropriate when catalog files are served with immutable caching.

When deployment localization is omitted—or the deployment config cannot be loaded—the selector automatically advertises every catalog included in the build, with en-US as the default. Supplying localization.locales explicitly replaces that fallback list, allowing an operator to restrict or reorder it. Locale catalogs never control the application name or other branding; branding.name in /.well-known/lightfe-config.json is the sole runtime branding authority, with lightfe as its built-in fallback.

Either place the selected JSON at <web-root>/.well-known/lightfe-config.json, or implement it directly in nginx while leaving the shared dist/ directory unchanged:

location = /.well-known/lightfe-config.json {
    default_type application/json;
    add_header Cache-Control "no-cache";
    return 200 '{"mode":"locked","allowedHosts":["shitposter.world"],"defaultHost":"shitposter.world","allowAdditionalAccounts":true,"branding":{"name":"Shitposter World","loginHtml":"<p>Welcome to <strong>Shitposter World</strong>.</p>","iconUrl":"/branding/shitposter-world.svg"}}';
}

When present, the policy endpoint must return HTTP 200 with Content-Type: application/json and should use Cache-Control: no-cache.

The policy is loaded and parsed in the data worker before session restoration. Locked deployments compare normalized hosts exactly, reject disallowed OAuth callbacks before token exchange, remove off-policy saved sessions, and constrain the login and account-switcher interfaces. Set allowAdditionalAccounts to false to retain only one local account.

Testing model

Components are pure: they take normalised, render-ready objects (see src/api/types.ts) as props or read module-scope stores seeded by seedMockData(). Nothing in src/components/ or src/routes/ fetches, so every screen is testable and previewable offline by construction — the same boundary DESIGN.md §3 requires for the worker split.

Notes / deviations

  • Search lives in the timeline headers (the spec's tab bar has four slots).
  • Status-content links use --accent — the spec restricts accent to non-text; recorded here as a deliberate old-Twitter-look exception, matching X's own link treatment.