- JavaScript 60.2%
- CSS 36.7%
- HTML 2.8%
- Python 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
These two notification types fell through to the raw `type (you)` fallback. - move: now reads "@old moved to @new" with the destination handle rendered as a profile link (hover-previewed and click-to-profile, like the actor), sourced from the notification's `target` account. - admin.sign_up: now reads "signed up" instead of "admin.sign_up (you)". Adds a generic inline "link" part type to the notification verb renderer (used here for the move target), wired with mark-read/navigate on click. |
||
| public | ||
| screenshots | ||
| tests | ||
| tools | ||
| .gitignore | ||
| AGENTS.md | ||
| Caddyfile.example | ||
| CHANGELOG.md | ||
| DEV_NOTES.md | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
chan-fe
Imageboard-style frontend for Mitra and Mastodon-compatible fediverse APIs.
chan-fe renders timelines as board catalogs with thread views, reactions, polls, media galleries, and posting tools. The application is a set of static files and does not require a server-side runtime of its own.
Status and compatibility
chan-fe is experimental, but actively usable for daily browsing and posting on a Mitra instance.
Mitra is the primary target and the only server exercised against in earnest. Several features use Mitra extensions, including custom post titles, emoji reactions, conversation visibility, and group-backed boards.
The frontend otherwise uses Mastodon-compatible /api/* and /oauth/*
endpoints. OAuth sign-in against Pleroma has worked, but broader
Pleroma/Mastodon compatibility has not been tested extensively. Expect rough
edges outside Mitra.
Highlights
- 4chan-style board catalogs and chronological thread views
- Mitra group-backed custom boards
- OAuth sign-in, posting, replies, editing, moderation, polls, and media uploads
- Likes, reposts, bookmarks, and Unicode/custom emoji reactions
- Inline reply and profile previews, media galleries, and opt-in media embeds
- Local hashtag catalogs linked from posts and federated search results
- Thread watcher and paginated fediverse notifications
- Configurable NSFW handling, activity previews, mention collapsing, and an Anonymous presentation mode
- No required build step
See CHANGELOG.md for the detailed initial feature inventory and future release notes.
Installation
Clone the repository onto the machine that will serve the frontend:
git clone https://codeberg.org/nak/chan-fe.git
cd chan-fe
The simplest deployment is to let Mitra serve chan-fe itself by pointing its
web_client_dir setting at this public/ directory — chan-fe then replaces
mitra-web as the instance's main frontend. See
Serve chan-fe from Mitra directly.
Alternatively, serve public/ from a separate HTTPS origin whose hostname is
the default Mitra instance, and reverse-proxy /api/*, /oauth/*, and
/media/* to Mitra. Either keeps normal API requests same-origin while still
allowing users to sign in to other compatible instances directly.
No Node.js installation or build step is required when serving public/
directly.
Configuration
Frontend configuration lives in public/config.js. It does not contain generated credentials or secrets; OAuth applications are registered dynamically with the selected instance.
OAUTH_CONFIG.defaultInstance defaults to the hostname serving chan-fe. This
is correct when that hostname reverse-proxies the Mitra endpoints described
above. If the frontend uses a different hostname, either set the default in
public/config.js or provide it to the optional production build:
npm run build -- --default-instance example.com
The configured instance is only the sign-in dialog's default. Users may still enter another compatible instance.
Board definitions, labels, and timeline API paths also live in
public/config.js.
Deployment
Serve chan-fe from Mitra directly
The simplest way to run chan-fe as your instance's main frontend is to let
Mitra serve it, replacing its default web client. Clone the repository onto
the Mitra host and point Mitra's top-level web_client_dir setting at the
public/ directory:
# Mitra config.yaml
web_client_dir: /srv/chan-fe/public
Restart Mitra. It now serves chan-fe at its own origin, so /api/*,
/oauth/*, and /media/* are same-origin and OAUTH_CONFIG.defaultInstance
— which defaults to the serving hostname — is correct with no extra
configuration. No separate web server or reverse proxy is needed; sign-in to
other compatible instances still works directly at their own origins.
For a minified bundle, run the production build and point web_client_dir at
dist/ instead:
npm install
npm run build
# then set web_client_dir to the dist/ directory, e.g. /srv/chan-fe/dist
Serve the static files directly
Point the web root at public/. Route API requests before the static-file SPA
fallback:
- Reverse-proxy
/api/*,/oauth/*, and/media/*to Mitra. - Serve existing static files.
- Fall back to
index.htmlfor client-side routes.
Caddyfile.example contains a reference configuration with the required route order and suggested security headers. Copy the relevant blocks into your existing Caddy configuration rather than replacing it wholesale.
If the static-file handler runs before the proxy rules, API requests may
receive index.html instead of JSON.
Optional production build
The optional build bundles and minifies JavaScript into dist/app.js, writes
an external source map, and copies the other static assets into dist/:
npm install
npm run build
Serve dist/ instead of public/ after building. To bake in an instance on a
different hostname:
npm run build -- --default-instance example.com
Deployment helper
The deployment helper builds and uploads dist/ with rsync:
npm run deploy -- example.com user@frontend.example:/var/www/chan-fe/
It uses --delete and --delay-updates. Check the destination carefully:
files at the destination that are absent from dist/ will be removed.
Screenshots
| Catalog | Thread view |
|---|---|
![]() |
![]() |
| Compose | Emoji picker | Media gallery |
|---|---|---|
![]() |
![]() |
![]() |
Development
There is no required development build step. Application code lives in
public/ and is plain JavaScript, HTML, and CSS.
Local static server
python3 tools/serve.py --port 8080
# open http://localhost:8080/
tools/serve.py sends Cache-Control: no-cache so browser refreshes reliably
pick up JavaScript and CSS edits.
You can also use Python's standard static server:
python3 -m http.server 8080 --directory public
When developing on localhost, set the default instance in public/config.js
or use the production build flag described above. The live instance must allow
the required cross-origin API requests unless a local reverse proxy is used.
Project layout
public/
├── index.html # app shell and modals
├── style.css # application styles
├── config.js # OAuth, board, and endpoint configuration
├── catalog.js # catalog sorting and thread-root helpers
├── threading.js # thread-root helpers for contexts and parent maps
├── media.js # media size, dimension, URL, and format helpers
├── gallery.js # multi-attachment gallery overlay
├── data.js # API access and timeline hydration
├── app.js # routing, state, rendering, compose, auth, and interactions
├── images/ # local presentation assets
├── css/ # vendored Font Awesome stylesheets
└── webfonts/ # vendored Font Awesome font files
tools/
├── build.mjs # optional production bundle
├── deploy.sh # build and rsync helper
└── serve.py # local no-cache static server
tests/
├── catalog.test.js
├── data.test.js
├── security.test.js
└── threading.test.js
screenshots/ # README screenshots
Testing
The Node built-in test suite covers the core pure-logic modules and URL-scheme security checks:
npm test
Watch mode:
npm run test:watch
See DEV_NOTES.md for architecture notes and browser verification guidance.
Security
- Fediverse post, profile, and instance HTML passes through a local tag, attribute, and URL-scheme allowlist before reaching the rendered DOM.
- YouTube and PeerTube frames are created only after an explicit click.
PeerTube URLs must be HTTPS canonical
/w/<video-id>links, are converted to constrained embed URLs, and run in a sandbox without forms, popups, top-level navigation, or downloads. - OAuth bearer tokens are stored in
localStorage. This keeps the static app simple, but means any same-origin script execution could read the token. Serve chan-fe over HTTPS, retain a restrictive CSP, do not add untrusted third-party scripts, and keep the frontend origin under your control. - The API wrapper attaches a bearer token only to the selected signed-in instance origin or the configured same-origin reverse proxy.
PeerTube embeds may originate on any HTTPS instance, so the example CSP uses
frame-src https:. Tighten connect-src, img-src, media-src, and
frame-src when deploying against a fixed set of instances.
Reference projects
- bloat-fe — authentication flow inspiration
- mitra-web — API behavior reference
- FChannel — federated imageboard server with a different architecture
License
chan-fe is released under the MIT License.




