- TypeScript 95.4%
- CSS 4%
- JavaScript 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .github/workflows | ||
| assets | ||
| public | ||
| scripts | ||
| src | ||
| test | ||
| .gitignore | ||
| AGENTS.md | ||
| DESIGN.md | ||
| eslint.config.mjs | ||
| FILTERING.md | ||
| index.html | ||
| lighthouserc.json | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| TODO.md | ||
| tsconfig.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
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 1–3 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/searchacross 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:
namereplaces the user-facinglightfename, browser title, and installed PWA name. It must contain 1–80 characters. It does not change the OAuth client name or internal storage keys.loginHtmladds 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.iconUrlreplaces 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:
- Translate a copy of
public/locales/en-US.json, retaining every message key and named placeholder. - Place it under the deployment web root, such as
/locales/ja-JP.json. - Add its
{code, label, url}entry tolocalization.localesin/.well-known/lightfe-config.jsonand optionally make itdefaultLocale.
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.