Self-hosted service
LemmyNet/lemmy-ui avatar
LemmyNet/lemmy-ui

lemmy-ui: the web app for a federated link aggregator

The official web app for lemmy.

1,015 stars380 forksCSSAGPL-3.0

At a glance

What is it?
lemmy-ui is the official web front end for Lemmy, a self-hostable link aggregator whose instances federate with each other so a user on one server can subscribe to communities on another. It is an isomorphic Node application that server-renders the same bundle the browser runs, is licensed AGPL-3.0, and is still labelled 1.0.0-beta.
Who is it for?
lemmy-ui fits an operator running a Lemmy instance for a community, or a developer who wants to contribute to a federated front end rather than another centralised one. It does not fit someone who wants a hosted Reddit replacement with no server to run, and it does not fit a team unwilling to carry an AGPL-3.0 codebase, since that licence reaches the whole application.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly CSS, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A front end for servers that talk to each other

Lemmy is described as a link aggregator and forum for the fediverse, similar to sites like Reddit, Lobste.rs or Hacker News, where you subscribe to forums you are interested in, post links and discussions, then vote and comment on them.

The difference is behind the scenes, and the README is clear about it: anyone can easily run a server, and all these servers are federated, think email, and connected to the same universe, called the Fediverse. For a link aggregator, that means a user registered on one server can subscribe to forums on any other server, and can have discussions with users registered elsewhere.

That is a protocol problem expressed as a product problem, and it is where the front end earns its keep. lemmy-ui is the official web app for that product, and the pieces it has to present are the ones federation creates: a post that originated on another instance, a comment thread whose author never touched this server, a community that is moderated somewhere else entirely. The README points at the backend repository for the parts that are not the front end.

The moderation model is part of the same design. Each Lemmy server can set its own moderation policy, appointing site-wide admins and community moderators to keep out the trolls and foster a healthy, non-toxic environment where all can feel comfortable contributing. The project positions itself as an easily self-hostable, decentralised alternative to Reddit and other link aggregators, outside of their corporate control and meddling.

For a reader, the practical consequence is that lemmy-ui is not a standalone app. It is one half of a system, and it talks to a backend over the network.

One bundle runs in the browser and on the server

The package metadata calls lemmy-ui an isomorphic UI for lemmy, and that word describes the whole architecture.

The production script makes it concrete. The run:prod script runs the translation generation, then the production build, and then starts the server with node --enable-source-maps dist/js/server.js. The same webpack bundle that the browser downloads also runs in Node, which is what an isomorphic application means: one codebase, two execution environments, and the server renders the page before the client takes over.

Why a link aggregator needs that is a federation problem again. A community hosted on another instance is slow to reach and may be slow to answer, and a front end that renders everything only in the browser makes the user wait for the whole page before seeing anything. Server rendering the shell of the page and streaming the rest is the standard answer, and the same answer works for a federated social application.

The environment variables make the two environments explicit, and they are worth reading as a pair. LEMMY_UI_BACKEND is the base url for lemmy-ui in the browser to connect to the lemmy backend server, and LEMMY_UI_BACKEND_INTERNAL is the base url for the lemmy-ui server to connect to the lemmy backend server, defaulting to LEMMY_UI_BACKEND. A deployment where the browser and the Node process reach the backend by different names therefore needs no code change, only configuration.

The environment variables are the deployment contract

The configuration section of the README is a table of environment variables, and it is the most operationally important part of the document for anyone running an instance.

LEMMY_UI_HOST is the IP and port the isomorphic node server is hosted at, defaulting to 0.0.0.0:1234. LEMMY_UI_FRONTEND is the base url where browsers can reach the lemmy-ui server, which is inferred from request headers when undefined, with the protocol optional. LEMMY_UI_BACKEND defaults to localhost:8536, and LEMMY_UI_BACKEND_INTERNAL mirrors it unless overridden.

The security-relevant entries are the last three. LEMMY_UI_HTTPS uses https:// as the default protocol for base urls and defaults to false, so an instance that terminates TLS at a reverse proxy has to say so. LEMMY_UI_DISABLE_CSP disables CSP security headers and also defaults to false. And LEMMY_UI_CUSTOM_HTML_HEADER injects a custom script into the head of every page, which is the documented way to add an analytics snippet or a custom widget, and also the documented way to introduce a script you do not control into every page your users load.

The debugging variables are separated out and are worth knowing about. LEMMY_UI_ERUDA enables Eruda, the console for mobile browsers, and defaults to true, and the README notes that in production Eruda partially violates the CSP but remains useful. LEMMY_UI_SOURCE_MAPS gives browsers and node the ability to report accurate stack traces, and it defaults to true as well. Both are described as disabled by default for production builds but able to be enabled explicitly, which is worth verifying against your own build settings rather than assuming.

LEMMY_UI_EXTRA_THEMES_FOLDER points at a directory of additional lemmy css themes, which is the mechanism for a server operator to add a house style without forking the front end.

Running it against the public test instance takes four commands

The development instructions are short, and they point at a public instance so that you do not need a backend to see anything work.

You need pnpm installed, then:

bash
git clone https://github.com/LemmyNet/lemmy-ui.git --recursive
cd lemmy-ui
pnpm install
LEMMY_UI_BACKEND=https://voyager.lemmy.ml pnpm dev

Then open http://localhost:1234 in your browser. This uses the public test instance https://voyager.lemmy.ml/ as backend, and for more details such as developing with a locally compiled Lemmy backend the README points at the contributor documentation.

The --recursive flag on the clone is not optional, and the reason is the translations. The repository carries a lemmy-translations directory that is a git submodule, and there is a whole family of scripts around it. translations:init runs git submodule init and then translations:update, and translations:update runs git submodule update --remote, and translations:generate runs node generate_translations.js, which is also run as a prebuild step before both the development and the production builds, and again by the dev script before the watch build starts.

The build itself is webpack with a commit hash injected, build:dev passes --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development and build:prod does the same with --mode=production, and the dev script adds NODE_OPTIONS='--enable-source-maps'. There is also a separate themes:build script that runs sass over src/assets/css/themes with --embed-sources, and an analyze script that runs webpack with --mode=none, which is the bundle size report.

Translations are a live pipeline, not a file you edit

The translation story is the most unusual part of the build, and it explains why the repository has a submodule and a script at its root.

The lemmy-translations directory is a submodule, and the update script pulls it with --remote, so the translation data comes from its own upstream repository rather than from this one. The translation status badge in the README points at Weblate, at weblate.join-lemmy.org, which means translations are contributed and merged through a translation platform rather than through pull requests to lemmy-ui.

That design has a clear benefit for a federated project with many locales: a translator contributes in Weblate, the translations repository is updated, and every front end instance picks the change up on the next build without anyone maintaining a dozen locale files in the UI repository. The cost is that a front end build now depends on the state of an external submodule, which is why the clone needs --recursive and why the build runs the generation script every time.

The lint script reflects the same care. It runs translations:generate first, then tsc --noEmit, then eslint with --report-unused-disable-directives, then prettier:check over the source and the configuration files. Husky is set up through the prepare script, and the .prettierignore and .prettierrc.json pin the formatting, so a contributor's editor and the CI agree on what the code should look like.

The Docker image compiles libvips rather than downloading it

The Dockerfile is a multi-stage build on node:current-slim, and the comment that explains the vips handling is the part worth reading.

vips-dev and pkgconfig are added so that local vips is used instead of prebuilt, and the Dockerfile comment gives two reasons: libvips binaries are not available for ARM32, and it can break depending on the CPU, with a link to issue 1566. So the image deliberately builds the native image library from source rather than pulling a prebuilt binary, which is slower to build and far more portable across architectures.

The rest of the build is careful about caching and reproducibility. node-gyp and corepack are installed globally, corepack is enabled so that pnpm is used, and the npm target platform is pinned to linux with the musl libc. The dependency install is cached through a mount on the pnpm store, with only package.json, pnpm-lock.yaml and pnpm-workspace.yaml copied in before it runs, and the source and the translations are copied afterwards. The.git directory is copied too, because the version is derived from it.

That version step is a nice detail. The build runs git describe --tag to get the current version, strips any suffix, and if the CI pipeline event is a cron it appends nightly and the date, and the result is written into src/shared/version.ts as an exported VERSION constant. So the same source produces a versioned nightly on the schedule and a tagged build otherwise, and the version the users see in the interface is generated rather than typed by hand.

The repository also has a dev.dockerfile for local development, a Woodpecker CI configuration in.woodpecker.yml, and a Docker Pulls badge pointing at the published image on Docker Cloud.

AGPL-3.0, a beta version, and releases published from the backend repository

The licence is AGPL-3.0, and it is stated in the package metadata, in the licence badge in the README, and in the LICENSE file at the root of the repository.

That licence choice has a consequence specific to this project, and it is a deliberate one. AGPL reaches a network service, so a modified Lemmy front end offered over HTTP to users must also offer its source to those users. For a project whose stated goal is to be an easily self-hostable, decentralised alternative to centralised platforms, keeping the front end and its modifications open is the point rather than an obstacle. An operator who wants to run a closed variant needs to know that before they fork, and the requirement is a matter of reading the licence rather than interpreting the README.

The version in package.json is 1.0.0-beta.2, so the front end is still labelled a beta after a long life, and the repository has no GitHub releases of its own. The releases are published from the backend repository instead, at LemmyNet/lemmy, with the Releases link in the README pointing at RELEASES.md there. The GitHub tag badge in the README header also points at the lemmy repository rather than at lemmy-ui, which is the same arrangement seen from the other side.

The last push to this repository was on 2026-10-14, and the build badge in the README points at Woodpecker, at woodpecker.join-lemmy.org, rather than at GitHub Actions. So the continuous integration, the translations, the releases and the front end all live in different places, coordinated by the Lemmy organisation rather than by a single repository. That is the shape of a federated project's tooling, and it is also the main operational thing to understand before running an instance.

Editorial conclusion

lemmy-ui fits an operator running a Lemmy instance for a community, or a developer who wants to contribute to a federated front end rather than another centralised one. It does not fit someone who wants a hosted Reddit replacement with no server to run, and it does not fit a team unwilling to carry an AGPL-3.0 codebase, since that licence reaches the whole application. Before you deploy, decide whether you will run the Docker image or build it, and if you build it, read the libvips note in the Dockerfile, because the prebuilt binaries the project avoids by compiling locally are unavailable on ARM32.

Frequently asked questions

What is lemmy-ui and what is it for?

It is the official web app for Lemmy, a link aggregator and forum for the fediverse whose servers federate with each other so a user on one server can subscribe to forums on any other server. The backend lives in a separate repository, LemmyNet/lemmy.

How do I run lemmy-ui locally?

Install pnpm, then clone with git clone https://github.com/LemmyNet/lemmy-ui.git --recursive, cd into lemmy-ui, run pnpm install, and start it with LEMMY_UI_BACKEND=https://voyager.lemmy.ml pnpm dev. Then open http://localhost:1234 in your browser.

Which environment variables configure lemmy-ui?

The README lists LEMMY_UI_HOST, LEMMY_UI_FRONTEND, LEMMY_UI_BACKEND, LEMMY_UI_BACKEND_INTERNAL, LEMMY_UI_HTTPS, LEMMY_UI_EXTRA_THEMES_FOLDER, LEMMY_UI_DISABLE_CSP and LEMMY_UI_CUSTOM_HTML_HEADER, plus the debugging variables LEMMY_UI_ERUDA and LEMMY_UI_SOURCE_MAPS.

How are translations handled in lemmy-ui?

The lemmy-translations directory is a git submodule, updated with translations:update using git submodule update --remote, and the generated translations come from the generate_translations.js script, which runs before the development and production builds and as part of the dev script. The translation status is tracked on Weblate.

What licence is lemmy-ui released under?

AGPL-3.0, declared in the package.json license field with the LICENSE file at the repository root. The package metadata names the description as an isomorphic UI for lemmy and the author as Dessalines.

Official sources

  1. Issues
  2. LemmyNet/lemmy-ui on GitHub
  3. License: AGPL-3.0
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lemmynet-lemmy-ui.svg)](https://hysenlabs.com/projects/lemmynet-lemmy-ui)

Was this useful?