webapp.rs: a Leptos and Axum rewrite of an all-Rust reference application
A web application completely written in Rust. 🌍
At a glance
- What is it?
- Seven years after its first release, this teaching project came back with a different frontend framework, a different web server and a different database. The metadata never got the memo.
- Who is it for?
- webapp.rs is worth reading as a worked example rather than as a framework to adopt. The v2 rewrite is a clean statement of the modern Leptos shape: one crate compiling to both a server binary and a WebAssembly client, server functions instead of a hand-written API, and Argon2 plus JWT for sessions.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the project is trying to demonstrate
The stated goal sits in the README's second line: write a complete web application, backend and frontend, entirely inside Rust. That is a narrower claim than it sounds. It is not a framework and not a product. It is a reference implementation that a reader can clone, build and read end to end, and the fact that the crate is called `webapp` while the repository is called `webapp.rs` tells you the author optimised for the docs.rs page rather than for the GitHub URL.
The numbers say the idea landed. 2,247 stars and 219 forks, with the last push on 2026-09-24 and no archived banner. Open issues sit at zero, which for a repository this size is either a sign of a well-tended issue tracker or one that has been closed to new entries. Either way there is no visible queue of unanswered questions.
The tree is small and deliberately legible: `src/` for the code, `migrations/` for schema, `public/` for static assets, `style/` for CSS, `tests/` for integration tests, and a single `Cargo.toml` at the root. The manifest declares the crate with `crate-type = ["cdylib", "rlib"]`, which is the load-bearing line for everything that follows. A `cdylib` can be compiled to WebAssembly for the browser; an `rlib` compiles natively for the server. One crate, two targets.
Three layers changed in the v2 rewrite
The v2.0.0 release, published 2026-03-24, is described as a ground-up rewrite. The release notes carry a table mapping each layer before and after:
| Layer | Old | New | |-------|-----|-----| | Frontend | Yew | Leptos (WASM with SSR and hydration) | | Backend | actix-web | Axum (via leptos_axum) | | Database | Diesel on SQLite | SQLx on PostgreSQL | | Auth | Basic | JWT with Argon2 password hashing |
Read that alongside the README architecture table and the two agree: Leptos on the frontend, Axum through `leptos_axum` on the backend, PostgreSQL through SQLx, JWT via `jsonwebtoken` with Argon2 for password hashing. The manifest confirms the same set at pinned versions, including `leptos 0.8.19`, `axum 0.8.9`, `sqlx 0.8.6`, `argon2 0.6.0` and `jsonwebtoken 11.0.0`, all behind an `ssr` feature flag with a matching `hydrate` flag for the browser side.
Here is the part worth pausing on. The repository topics are still `actix-web`, `diesel`, `diesel-rs`, `backend`, `frontend`, `http-server`, `postgresql`, `rust`, `webapp` and `website`. Three of those name a framework and an ORM the v2 code no longer uses. The README's own diagram agrees with the release notes, so the mismatch is not a disagreement between documents, it is a stale metadata field. Practical consequence: a package index, a search engine or an agent that reads topics will describe this as an actix-web and Diesel project, and every one of those descriptions will be wrong. Trust `Cargo.toml`, not the topics list.
Why server functions change the shape of the code
The README is explicit that Leptos server functions handle communication between frontend and backend, which removes a separate REST layer. In practice this means the `#[server]` attribute generates a function that exists as a real Rust function on the server and becomes an HTTP call when compiled to WebAssembly. You call `login(username, password)` in one place and it runs in-process on the server or over the network from the browser, depending on which target you built.
That has a real payoff for a teaching project. There is no API surface to define, serialise by hand or keep in sync with client types, because the type system is what keeps them in sync. It also removes the place where a lot of production bugs live, which is the hand-maintained request and response contract.
The cost is that you give up a stable public API. If you were planning to call this application from a mobile client or a script, the server function approach does not help you, because the RPC layer is generated for the bundled browser client rather than published as an interface. The README does not claim otherwise, and it is worth reading this repository as a reference for how a Leptos application fits together rather than as a service you intend to consume.
The version number is a signal of how much moved: 2.0.0, not 1.1.0, and the release body says complete rewrite. Semver is being used to say that every layer of the previous application is gone.
The seven years between the two releases
v1.0.0 shipped on 2019-07-05 with a release body that reads, in full, "Initial release". v2.0.0 shipped on 2026-03-24. That is a gap of just under seven years between the two, during which the repository accumulated stars but not releases.
For a reader this matters less as a criticism than as a warning about the current version line. Everything you learn from v1 tutorials about Yew, actix-web and Diesel applies to code that no longer exists in the tree, and there is a lot of it online because the project was popular in 2019. Search results for this repository will surface v1-era material, and the v2 manifest will look alien against it.
The rewrite also explains the metadata staleness. Repository topics are rarely revisited after a stack change, and three of the ten here name the old stack. The pinned dependency versions tell you where the effort went: `edition = "2024"` in the manifest means the code targets a recent Rust release, and the Leptos 0.8 line is a current generation rather than a legacy one. So the code is modern and the labels are not. When you evaluate this project, read the manifest first and the topic list never.
Getting it running locally
The prerequisites are a stable Rust toolchain, `cargo-leptos`, a PostgreSQL server, the `wasm32-unknown-unknown` target and `wasm-bindgen-cli`. The database comes up in one container:
docker run -d --name postgres \
-e POSTGRES_USER=webapp \
-e POSTGRES_PASSWORD=webapp \
-e POSTGRES_DB=webapp \
-p 5432:5432 \
postgres:17Then the application itself, with the connection string pointing at it:
export DATABASE_URL=postgres://webapp:webapp@localhost/webapp
cargo leptos watchThe README says the application is then available at `http://127.0.0.1:3000`, and you register an account through the Register link on the login page before logging in. There is no seed data and no fixture, so the registration path is the only way in.
`cargo leptos watch` is doing the work here. Because the crate builds twice, once natively and once to WebAssembly, a plain `cargo run` is not sufficient. The tool builds both halves and wires the static assets into the server binary's output. The development command set in the README is:
cargo fmt --check # Check formatting
cargo clippy --features ssr -- -D warnings # Lint server code
cargo test --features ssr # Run tests
cargo leptos build # Build for development
cargo leptos build --release # Build for productionThe clippy line is the informative one: linting runs with the `ssr` feature and denies warnings, which is how a codebase that compiles to two different targets keeps the target-specific code honest.
Configuration, and the secret you should change
Three environment variables are documented, each with a default:
- `DATABASE_URL`, defaulting to `postgres://localhost/webapp` - `JWT_SECRET`, defaulting to `change-me-in-production` - `LEPTOS_SITE_ADDR`, defaulting to `127.0.0.1:3000`
The `JWT_SECRET` default is the one to notice. A placeholder that says "change me in production" is the right instinct, printed in a README table, but it is still a shipped default. Anyone who deploys this without setting it signs and verifies tokens with a key that is public in the repository. If you fork this for anything, treat setting `JWT_SECRET` as a deployment blocker rather than a recommendation.
`DATABASE_URL` has the same shape of problem in a milder form. The default has no credentials and no host, so it will not connect to anything on its own, which is a mild mercy. The container path is better here because the Dockerfile does not set a `JWT_SECRET` at all, meaning a container started exactly as the README describes will fall back to the placeholder unless you supply one.
The third variable is where the local and container behaviour diverge, which is the subject of the next section.
The container is not a single binary
The README feature list advertises single binary deployment, and the container section is short:
docker build -t webapp .
docker run -p 3000:3000 \
-e DATABASE_URL=postgres://webapp:[email protected]/webapp \
webappWhat the Dockerfile actually does is a four-stage cargo-chef build. A `rust:latest` base installs the WebAssembly target plus `cargo-chef` and `cargo-leptos`, a planner stage runs `cargo chef prepare` to produce a recipe, a builder stage cooks dependencies twice, once for the native target with `--features ssr` and once for `wasm32-unknown-unknown` with `--features hydrate`, then runs `cargo leptos build --release`. The final stage is `debian:bookworm-slim` with `ca-certificates` and `curl` installed.
That final stage copies two things, not one: the `webapp` binary to `/usr/local/bin/webapp` and `target/site` to `/app/site`. The image sets `LEPTOS_SITE_ROOT=site`, which is why the directory has to be there. So the deployable artifact is a Debian base image with a Rust binary and a directory of static assets, not a self-contained single file you can copy anywhere. The single binary claim is true of what cargo produces and imprecise about what ships.
Two more details in that stage matter operationally. It sets `LEPTOS_SITE_ADDR=0.0.0.0:3000`, so the container listens on every interface, which is what makes the published port mapping work. And it declares a `HEALTHCHECK` that curls `http://localhost:3000/healthz` on a 30 second interval with a 3 second timeout and 3 retries, matching the health endpoint the release notes describe as verifying database connectivity. That is a well-formed container health check, and it means the endpoint is worth reading before you trust it.
Two configuration facts that do not match
The configuration table lists `LEPTOS_SITE_ADDR` as `127.0.0.1:3000`. The Dockerfile sets `0.0.0.0:3000`. Both are correct for their context, and both appear in this repository, so it is worth knowing which one you are running.
Locally, `cargo leptos watch` binds loopback only, which is the right default for a developer machine. In the image, the same variable binds every interface, which is what a port mapping needs in order to be reachable from outside the container. The mismatch is not a bug. But the README's configuration table does not mention the container override, so anyone reading the table alone will assume loopback and then be confused when a published port works.
The second mismatch is in the run command. The example uses `host.docker.internal` in the connection string, which resolves on Docker Desktop for macOS and Windows. On Linux, where `host.docker.internal` does not exist by default unless you add the host gateway mapping, the same command fails to connect. If you are running the container on Linux, either add `--add-host=host.docker.internal:host-gateway` or point `DATABASE_URL` at the database container's name on a shared network.
Both of these are small things, and both are the kind of detail that costs an afternoon if you hit them cold. They are also the argument for reading this repository's Dockerfile rather than only its README.
The security posture in one screen
The release notes list the defences that were added during the rewrite, and they are a reasonable set for a demo: Argon2 password hashing with unique salts, JWT session tokens with automatic renewal and server-side session tracking, CSRF protection through Origin and Referer header validation, per-IP rate limiting on a sliding window of 20 POST requests per minute, periodic cleanup of expired sessions, and a health check that verifies database connectivity.
The rate limit number is worth internalising. Twenty POST requests per minute per IP is a deliberately low ceiling, appropriate for a login and registration endpoint and far too low for any real API. It is a security control sized for a reference application, and if you copy the pattern you need to resize it.
Session state lives in PostgreSQL alongside user records, which means sessions survive a process restart but also that logout is a database write rather than a token discard. Automatic renewal is the interesting detail: a JWT that never expires is a liability, and one that silently extends its own validity trades a shorter window for less user friction. Whether the renewal interval is long enough to be useful and short enough to matter is a parameter you should find in the source rather than guess at.
Taken together, this is more hardening than a typical teaching project ships. That is a sign the author treats it as something people might actually run, which raises the stakes on the default `JWT_SECRET` rather than lowering them.
Editorial conclusion
webapp.rs is worth reading as a worked example rather than as a framework to adopt. The v2 rewrite is a clean statement of the modern Leptos shape: one crate compiling to both a server binary and a WebAssembly client, server functions instead of a hand-written API, and Argon2 plus JWT for sessions. Two things deserve a second look before you copy any of it. The repository topics still advertise actix-web and Diesel, so anything that reads metadata rather than the manifest will get the stack wrong. And the container listens on every interface while shipping a default JWT secret in the README table, which is fine for a local demo and wrong for anything else.
Frequently asked questions
What is webapp.rs and what is it for?
It is a complete web application written entirely in Rust, intended as a reference implementation rather than a framework or a product. The stated goal is to build backend and frontend in one language and one crate. Useful if you want to see how Leptos, Axum, SQLx, JWT and Argon2 fit together in a project small enough to read in an afternoon. Not useful if you want a dependency you can adopt, because it is an application, not a library.
Which web framework does webapp.rs use?
Leptos on the frontend with server-side rendering and hydration, and Axum on the backend through the leptos_axum integration. Both come from the same crate: the manifest declares crate-type as cdylib and rlib, so the browser build is WebAssembly and the server build is native. Which one you get depends on whether you compile with the hydrate or the ssr feature.
Does webapp.rs expose a REST API?
No, and this is deliberate. Leptos server functions handle frontend to backend communication, so there is no separate REST layer to define or maintain. The tradeoff is that you lose a stable public interface. Code calling this application from outside a browser has no published contract to target, so the project suits reading rather than consuming.
Why do the repository topics mention actix-web and Diesel if the code uses Axum and SQLx?
Because the topics list was not updated after the v2.0.0 rewrite. The release notes, the README architecture table and Cargo.toml all agree on Leptos, Axum, SQLx and PostgreSQL. Only the topic labels still name the old stack. Anything that reads metadata instead of the manifest will describe this project incorrectly, so check Cargo.toml before trusting any summary of it.
Is the Docker deployment a single binary?
Not quite. The cargo build produces a single binary, and the image copies it to /usr/local/bin/webapp, but it also copies the built static site to /app/site and sets LEPTOS_SITE_ROOT to point at it. The final image is debian:bookworm-slim with the binary, the asset directory, ca-certificates and curl. Treat it as a small container image rather than a portable single file.
What should I change before deploying webapp.rs?
Set JWT_SECRET first. The README configuration table ships the placeholder change-me-in-production as the default and the Dockerfile does not set it, so a container started as documented signs tokens with a key that is public in the repository. Also check the listen address: the README table says 127.0.0.1:3000 while the image sets 0.0.0.0:3000, so the container is reachable on every interface by design. On Linux, host.docker.internal in the example connection string needs an explicit host gateway mapping.
Official sources
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.
[](https://hysenlabs.com/projects/saschagrunert-webapp-rs)