A batteries-included web framework for Rust, inspired by Laravel: Axum, HTMX and Alpine.js, SQLite or PostgreSQL, deployed as one binary.
Rust
0
617 commits
updated Oct 11, 2026
Laravel's productivity, Rust's performance, one binary to deploy.
A web framework is a toolbox for building websites and web apps, so you don't start from zero. Renox is one for Rust, and it is "batteries included": the pieces most apps need (pages, forms, logins, a database, email, background jobs) come in the box and fit together.
Under the hood, Renox uses well-known parts. Axum is the web server library. HTMX and
Alpine.js are two small scripts that make pages update in the browser without a reload.
SQLite (or PostgreSQL) stores the data. You add one dependency, renox, and you get all of it.
Read the docs at docs.renox.rs, or start at renox.rs. New here? Start with the tutorial: it builds one small app step by step and explains every word. Already know Laravel? Read coming from Laravel; every guide is listed under Documentation. API reference: docs.rs/renox.
rnx build packs your pages
(views), translations, CSS/JS and database changes (migrations) into that one file. Copy it
and a .env settings file to a server, and you're done. You don't need Node, Redis or a
separate program for background jobs. rnx make:deploy writes the Dockerfile, a systemd
unit and Litestream backups for the SQLite file.
The examples/hello guestbook: Rust validation, errors inline, the list swapped in by htmx, and no page reloads.
You need Rust 1.94 or later (rustup installs it) and a C compiler (for
SQLite; on Linux build-essential or your distribution's equivalent, on macOS
xcode-select --install).
cargo install renox-cli # installs `rnx`, Renox's command-line tool
rnx new blog && cd blog # or: --starter, --database postgres, --tailwind
rnx serve # http://127.0.0.1:3000
An app's name uses lowercase letters, digits, - and _, and starts with a letter (blog,
coffee-shop). Rust keywords and the names of crates the app uses (renox, serde, tokio,
std, test, …) are refused, so rnx new MyApp and rnx new test fail.
For the latest main instead of the release: cargo install --locked --git https://github.com/arif-rachim/renox renox-cli.
Then, in the app:
rnx make:module posts --resource --fields "title:string body:text published:bool".
rnx serve rebuilds and restarts by itself, and the browser reloads when a view changes.cargo test. rnx --help lists rnx's own commands (rnx help prints
the same). The app's commands, built-in and your own, are listed by the app itself:
cargo run -- help.The new app has a layout built with the UI kit, Renox's ready-made page parts (navigation bar,
account menu, toasts: small pop-up messages), a home
page, login and registration, an account page (profile, password, other devices), an error page
in the layout, its texts in resources/lang/en.json, a test in tests/home.rs, and an
AGENTS.md and a CLAUDE.md for coding agents. rnx new blog --starter writes the starter kit
on top: a sidebar layout with the notification bell, email verification, roles, a dashboard, a
users page for admins and the activity log, with their tests. To get admin access there, sign
up at /register and run rnx users:admin you@example.com, or run rnx db:seed and log in as
admin@example.com with the password password123. With --database postgres,
create the blog and blog_test databases first (or edit .env).
[!NOTE] The first build compiles every dependency and takes a few minutes. Later builds take seconds. See docs/development.md for faster builds.
A coding agent is an AI assistant that writes code in your project. Renox is made to be easy for them: there is one way to do each thing, generators write the repetitive code (boilerplate), and the docs' code examples are compiled in CI (the automatic checks on every change), so they always match the real code.
In an app made by rnx new, there is nothing to set up. The app has an AGENTS.md (read by
Codex, Cursor, Copilot, Gemini CLI and others) and a CLAUDE.md that imports it (read by Claude
Code). They tell the agent which rnx make:* generators to use, where the cheat sheet, guides
and examples of the Renox version the app uses are, the app's layout, and the traps agents
have hit. Start the agent in the app's folder:
cd blog
claude # or your agent of choice
and ask for what you want, for example:
Add products with a name, a price and a photo: a list with search and pagination, a form that validates, only admins may edit. Use the generators, then write tests and run
cargo test.
In another project, or when the agent doesn't know Renox yet, point it at the docs. Put
this in the project's CLAUDE.md (or AGENTS.md):
This app uses Renox, a Laravel-like Rust web framework (https://github.com/arif-rachim/renox).
Before writing Renox code, read
https://raw.githubusercontent.com/arif-rachim/renox/main/llms.txt: it maps every topic to the
guide or example that shows the official way (its links are paths in that repository).
Prefer `rnx make:*` generators, `use renox::prelude::*`, and the UI kit (`renox/ui.html`) for
pages. Run `cargo test` after changes.
Claude Code, set up once per app (optional, both make it faster):
Give it Renox's docs and examples on disk, at the version your app uses, so it reads them instead of fetching or guessing:
git clone --depth 1 --branch v1.0.0 https://github.com/arif-rachim/renox ~/src/renox
claude --add-dir ~/src/renox # or, inside a session: /add-dir ~/src/renox
Let it build, test and run the generators without asking each time, in the app's
.claude/settings.json (commit it, so the whole team gets it):
{
"permissions": {
"allow": [
"Bash(cargo build:*)",
"Bash(cargo check:*)",
"Bash(cargo clippy:*)",
"Bash(cargo test:*)",
"Bash(cargo fmt:*)",
"Bash(rnx make:module:*)",
"Bash(rnx make:model:*)",
"Bash(rnx make:migration:*)",
"Bash(rnx migrate)",
"Bash(rnx migrate:status)"
]
}
}
name:* allows every command that starts with name, so rnx migrate is listed exactly:
commands that drop data or reach the outside world (rnx migrate:fresh, rnx db:seed,
deploys) are better left to ask.
Tips:
~/.cargo/registry/src/*/renox-core-*/), so
an agent can read the real code instead of guessing.renox::testing::TestApp drives the app without a server,
so cargo test checks pages, forms, mail and jobs in seconds.Here is a small guestbook. A form is checked (validated) in Rust, a new row is saved in the database, and only the updated list is sent back, so htmx swaps it into the page without a reload:
use renox::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Model, Serialize, Default)]
#[model(table = "entries")]
struct Entry { id: i64, name: String, message: String }
#[derive(Deserialize)]
struct EntryForm { name: String, message: String }
impl Validate for EntryForm {
fn rules(&self, v: &mut Validator) {
v.field("name", &self.name).required().max(50);
v.field("message", &self.message).required().between(3, 280);
}
}
// Bad input never gets here: htmx gets a 422 and the errors appear next to the fields.
async fn store(State(db): State<Db>, Valid(form): Valid<EntryForm>) -> Result<View> {
// Save the new entry as a row in the `entries` table.
Entry::create(&db, Entry { name: form.name, message: form.message, ..Default::default() }).await?;
// Read every entry back, newest first.
let entries = Entry::query().latest().get(&db).await?;
// Render only the template's `entries` block; htmx puts it into the page.
Ok(view("guestbook.html", context! { entries }).fragment("entries"))
}
// Routes live in modules, registered with `App::new().module(Guestbook)`.
struct Guestbook;
impl Module for Guestbook {
fn name(&self) -> &'static str { "guestbook" }
fn routes(&self) -> Routes {
Routes::new().post("/entries", store).name("entries.store")
}
}
<form hx-post="/entries" hx-target="#entries" hx-swap="outerHTML"> <!-- CSRF sent for you -->
<input name="name"> <textarea name="message"></textarea> <button>Send</button>
</form>
{% block entries %} {# all htmx gets back #}
<ul id="entries">{% for e in entries %}<li>{{ e.name }}: {{ e.message }}</li>{% endfor %}</ul>
{% endblock %}
Everything below is built in. Click a heading to open it. Words in code are the names you
type in Rust, templates or the terminal.
/products, run this function") are grouped in
modules and in prefixed groups (Routes::group("/admin", "admin.", …)), with names
(route('products.edit', id) in templates), guards that decide who may open them
(.require_auth(), .guest_only(), .require_verified(), .require_gate("admin"),
.require_role(…), .require_permission(…), .require_ability(…) for API tokens,
.require_password_confirmed()) and rate limits that cap how often someone may call them
(.throttle(60, Duration::from_secs(60)), or a named limiter that picks the limit per user or
API key: .throttle_by("api")).Routes::domain("{account}.example.com", …)), a fallback for what
nothing else answers, redirects by route name (Redirect::route, Redirect::intended), and
the current route in views (route_is('admin.*')) and handlers (CurrentRoute).Found(post): Found<Post> loads the row {post}, {id} or {slug}
names (404 if there's none, within the tenant's default scope). Pages without a handler
(.view("/about", "about.html"), .redirect(…)), and .etag() for 304s on feeds and lists.SESSION_DRIVER=database). Flash messages (shown once, on the next
page), old input (what the visitor typed, kept after an error) and list/counter helpers
(session.push, session.increment) come built in. CSRF protection, which stops other sites
from sending forms in your visitors' name, is automatic for forms and htmx._method spoofing lets plain HTML forms, which can only send GET and POST, send PUT and
DELETE.TRUSTED_HOSTS refuses other hosts, and App::xsrf_cookie() hands the CSRF token to
JavaScript clients as Laravel's XSRF-TOKEN cookie.impl Webhook, .webhook::<W>(path)):
signature checks (HMAC, Stripe-style), each event stored and processed once in the queue,
with webhook:retry when something failed.my-app down --secret …) and /health are included.Cookies, SetCookie), downloads and streamed responses
(Download), and abort(StatusCode::GONE, "…") for any status with a message.App::layer,
Routes::route_layer), shared services (App::provide), template filters (App::templates),
data for every view (App::share) and async gates..fragment("block") answers htmx with just one block of the page, not the whole page.
HxTrigger, HxRedirect and Back cover the rest.{{ csrf_field() }}, {{ method_field('PUT') }}, old(), error(), t() and route() work
in every template (route('products.index', q=q) adds a query string), can('update', product) on models the handler wrapped with Can::new, and
pagination(products) once imported from renox/pagination.html.asset('app.css') adds a content hash (?v=…) that changes when the file changes, so
browsers can keep (cache) assets for a year and still get new versions.<meta> into the layout's
{{ stack('scripts') }} / {{ stack('head') }}, once if asked ({% call push('scripts', once='chart') %}).rnx new --tailwind, and rnx serve / rnx build run Tailwind's
standalone CLI (downloaded once, checked by SHA-256).old, error, t, can, auth), and a UI kit
ships with Renox (renox/ui.html), with a warm default theme (Inter and Poppins bundled, a
type scale that puts the important figure first; a classic theme too):
lines[0][qty]) read into a Vec of structs;APP_CURRENCY),
dates, "3 hours ago", badges, Yes/No, swatches, pairs or Markdown;Toast::success(…), with a body, links, a duration and a position) and live
validation.renox::chart): figures with their change and a
sparkline, line, area, bar, pie and doughnut charts drawn on the server as HTML and SVG
(crosshair tooltips, keyboard, a data table, colours checked for colour blindness), values
per day or month from a query (Trend::of(query, "created_at").over(period).sum(…)), a
period filter, and widgets that load on their own and refresh.renox::grid): filters per column by kind (a date range
calendar for dates), server pages and sorting in the URL, grouped headings, frozen columns,
different columns on phones and desktops kept per user, and cells drawn by the page
({{ sparkline(…) }} charts, buttons); columns moved and resized by dragging, details with audit fields, editing in place, rows
dragged into order, merged cells, and CSV/Excel/print exports. Also a search box, filter
chips, rows that link to a page, several grids on one page, bulk and row actions, summaries
and groups, cards on phones, image/badge/link/copyable cells, columns from related tables,
an advanced filter, state remembered in the session and polling (guide)..also("block") sends out-of-band blocks with a fragment; HxRetarget, HxReswap and
HxPushUrl set the other htmx headers.#[derive(DbEnum)]), multi-selects (Json<Vec<_>>), dates, times, JSON, UUIDs
(docs/types.md).migrate, migrate:rollback, migrate:fresh --seed), with per-database files when SQL
differs.#[derive(Model)]
gives it create, save, delete (with optional soft deletes), find_or_404,
a query builder (OR groups, sub-queries, where_has, aggregates, group_by/having, raw
fragments, row locks, bulk updates, upserts, update_or_create, chunks), pagination (numbered,
simple or by cursor) and factories with fake data. The key is the id field's type: a
number the database counts, or a ULID, UUID or string
(rnx make:model Invoice --module invoices --key ulid; the module must exist).save_changes, save_only), run hooks (saving, saved,
deleting, deleted), carry a default scope, e.g. the current tenant, that every query
applies until unscoped(), and keep secrets encrypted at rest (Encrypted<String> fields,
sealed with APP_KEY). Transactions nest with savepoints (tx.savepoint(…)).belongs_to, has_many, many-to-many pivots (with
pivot columns) and polymorphic Morph load a page's related rows in one query each (two for
pivots: the pivot rows, then the models), and
count_many / sum_many give counts and sums per row (guide); joins
read into #[derive(FromRow)] structs with fetch_as.tsvector): #[model(search = "title, body")], a migration from
renox::db::search::migration::<Post>, then Post::search(&q) with any other filter and
pagination; the database keeps the index current, and the data grid's search box uses it
(guide).? placeholders, and transactions that can retry on a
busy database (db.transaction_retrying(3, …)): renox::db::sql("…").bind(x).fetch_all(&db).Valid<T> validates forms,
JSON bodies and query strings with rules such as required,
required_if, email, between, matches (regex), digits, dates (before, after),
unique, exists, same, gt/lt against another field, decimal, alpha_dash, uuid,
json, timezone, size, image, mimes, dimensions (pixels) and current_password, per item of
a list (each, nested, distinct), and your own reusable Rules. Messages come in English,
or from your own translations (resources/lang/<locale>.json). Simple forms declare them as attributes:
#[derive(Validate)] with #[validate(required, email, unique("users", "email"))].prepare tidies the input, authorize answers 403 before any rule, and
after runs checks that need the database, with errors shown like a rule's.Auth::new() adds login, registration, logout, remember me, password reset and email
verification, with Argon2id password hashing (passwords are stored scrambled, never as typed)
and login throttling, on pages built with the UI kit;
.account() adds a profile page
(email change with re-verification, password, "log out other devices", delete account).
Logout ends only this device. Password rules come from Password::min(12).mixed_case()…
(.uncompromised() refuses passwords from known breaches, asking Have I Been Pwned by hash
prefix), and
users imported from Laravel log in with their bcrypt hashes.renox-2fa crate: one module, and users turn on codes
from an authenticator app on their account page, with recovery codes
(guide).LoggedIn, LoginFailed, Registered, …) and an opt-in Audit module that
records them, plus your own entries (audit::record).Authorization: Bearer) with abilities and expiry serve mobile apps and
integrations.user.authorize("update", &product)? in
handlers, can(...) in views, App::gate_before for super-admins, and an opt-in
Permissions module with roles and permissions (user.assign_role(db, "editor"),
.require_role("editor")).queue:failed / queue:retry.
On PostgreSQL, workers on several servers never take the same job. Queues drain in priority
order (--queue high,default); jobs can be unique, encrypted, rate limited or kept from
overlapping, have a failed hook, and run in chains or in batches with progress.every_minutes(5, …), daily_at("02:00", …),
cron("30 9 * * 1-5", …), weekly_on, monthly_on, with weekdays(), between(…),
on_failure(…)). It runs inside serve in APP_TIMEZONE or a task's own IANA zone, daylight
saving included, and each run is claimed once when several servers share the database./_renox/mail while developing. More
mailers by name (App::mailer), and MAIL_FAILOVER to a second provider when the first
is down.t() and components (button, panel, table).
Auth::new().notifications() adds a bell for the navigation bar: unread badge, a panel to
read, mark and delete them, and new ones arriving live over Server-Sent Events./_renox/queue (behind a gate) shows waiting jobs, throughput, failed
jobs to retry or forget, and batch progress.state.http calls other services with timeouts and retries; in tests, app.fake_http()
answers instead and no request reaches the network. Scheduled tasks can ping health checks.storage.list, copy and rename work on both. More disks,
each with a name (App::disk("backups", …), state.disk_named("backups")), sit next to the default.remember, put, forget, add,
pull, increment). It lives in memory or in the database, with atomic locks
(state.cache.lock("stock:42", ttl)) that hold across servers on the database store.App::detect_locale()), from resources/lang/*.json, with t(), plurals and
Laravel's plural ranges. Renox's own texts are English; the same files translate them (ui.*,
renox.auth.*, renox.validation.*).{{ seo(title=…, description=…, image=…) }} writes the title, description, canonical URL,
OpenGraph and Twitter card tags.robots.txt is generated for you, and Sitemap builds sitemap.xml from routes and models.
Staging servers say noindex. /favicon.ico answers a quiet 204 until you add one..env, CSP-ready with nonces, and
only in production.analytics::event(&session, "sign_up", …) reaches gtag with the htmx swap, the page or the
next page. ServerEvent sends from the server through the Measurement Protocol, where ad
blockers can't drop it.use renox::prelude::*;
use renox::testing::TestApp;
#[renox::test]
async fn login_page_and_registration_errors() {
let app = TestApp::new(App::new().module(Auth::new())).await; // fresh DB, fake mail and queue
app.get("/login").await.assert_ok().assert_see("Log in");
app.htmx().post("/register", &[("email", "nope")]).await.assert_invalid("email");
}
rnx make:module products --resource --fields "name:string price:money" # a whole CRUD with tests
rnx make:module news --resource --model Article # when the model's name isn't the module's in the singular
# also make:model -m, make:policy, make:job, make:factory, make:test…
rnx make:command orders:close --module orders # a typed command (clap): --help, checked arguments, prompts
rnx route:list # every route with its name, module and guards
rnx db:shell # SQL prompt, no sqlite3/psql needed
rnx build && rnx make:deploy # dist/blog + Dockerfile, systemd (+ socket), Litestream
rnx make:deploy also writes a systemd socket unit: deploys then restart the app without
refusing a single connection.
In production, timeouts keep a slow database or mail server from holding requests, /health
feeds your load balancer, and panics in handlers, jobs and tasks are contained. CI checks this by
stopping, pausing and locking the database under a running app
(running in production). Tests are covered in docs/testing.md.
Every request gets an id that's in its log lines and its X-Request-Id. Logs can be JSON
(LOG_FORMAT=json) or go to a file. App::report hands 500s, failed jobs and failed tasks to
Sentry or a chat channel. Error pages use the app's layout. While developing,
/_renox/debug shows the last requests with their SQL, and flags N+1 queries.
rnx new include an AGENTS.md (and a CLAUDE.md importing it) that tells an
assistant how the project works; see Use Renox with Claude Code.| Renox | Loco | Axum on its own | |
|---|---|---|---|
| Inspired by | Laravel | Rails | (a library, not a framework) |
| Front end | Server-rendered + htmx + Alpine, bundled, no Node | Server-side templates, or a client-side app built with npm | Up to you |
| Data | Its own light model layer on sqlx; SQLite first, PostgreSQL optional | SeaORM entities | Up to you |
| Jobs | Queue in your database (SQLite or PostgreSQL), run inside the same binary | Queue in Redis, PostgreSQL or SQLite, or in-process tasks | Up to you |
| Auth, mail, uploads, i18n | Built in, with pages and translations | Mailers and storage built in; auth comes with the SaaS starter | Assemble from crates |
| Deploy | One binary with its assets + .env; Dockerfile, systemd, Litestream generated | Binary + config; Dockerfile, nginx or AWS Lambda generated | Up to you |
| Maturity | 1.0 on crates.io, one maintainer | Released on crates.io, larger community | Mature, widely used |
Choose Loco if you prefer Rails conventions, SeaORM or a JavaScript front end. Choose Axum on its own if you want to assemble every piece yourself. Choose Renox if you want Laravel's everything-included workflow and HTML over the wire, deployed as a single file.
Two examples, both on the UI kit:
examples/bikeshop: the one complete use case. A bike shop with
three stores that sell, rent and service bikes, on real-sized data. It has:
/about/fields (every input type ↔ Rust ↔ SQLite ↔ PostgreSQL) and /about/htmx
(every htmx recipe, live);Every page explains itself beside its own code: from 1200 px, "About this page" is docked next to the page with the handler, template and queries behind it, cut from the source at build time. Live at bikeshop.renox.rs. Start here.
examples/hello: the smallest app, the guestbook from the GIF. It has an
HTMX form, a photo upload, an event that queues mail, a scheduled task, English and Spanish
texts, login and an account page.
| Laravel | Renox |
|---|---|
php artisan | rnx (rnx make:model, rnx migrate, rnx route:list, …) |
Route::resource, make:controller --resource | Routes::resource, rnx make:module --resource |
Event::fake, Notification::fake, $this->travel() | app.fake_events(), app.fake_notifications(), app.travel(…) |
| Blade | MiniJinja templates, with {% extends %} and {% block %} |
| Blade components, Breeze's UI | Macros that see the request (rnx make:component), the renox/ui.html kit |
routes/web.php, Route::prefix()->name()->group() | Module::routes, Routes::group("/admin", "admin.", …) |
Route::domain, Route::fallback, redirect()->route(), ->intended() | Routes::domain(…), Routes::fallback(…), Redirect::route(…), Redirect::intended(…) |
HasUlids / HasUuids, encrypted casts, nested transactions | id: Ulid / id: Uuid, Encrypted<T> fields, tx.savepoint(…) |
routeIs, @class, trans_choice ranges | route_is('admin.*'), class_names(…), {0} none|[1,*] :count in lang files |
| Factory states and sequences | Product::factory().count(3).state(f).sequence(|i, p| …) |
Route model binding, Route::view, Route::redirect | Found<Post>, .view(…), .redirect(…) |
Several disks (Storage::disk('s3')) | App::disk(name, …), state.disk_named(name) |
Several mailers, the failover transport | App::mailer(name, …), state.mailer_named(name), MAIL_FAILOVER |
hasManyThrough, named error bags | relations::has_many_through, #[validate(bag = "login")] |
| Middleware | .require_auth(), .throttle(…), Routes::route_layer, App::layer |
RateLimiter::for('api', …) | App::rate_limiter("api", …) and .throttle_by("api") |
Exception reporting (report()), Telescope/Debugbar | App::report(…), /_renox/debug |
| Eloquent | #[derive(Model)] and the query builder; relations are explicit loaders (docs/relations.md) |
Form Requests (authorize, prepareForValidation, after) | Valid<T> with impl Validate (authorize, prepare, after), or #[derive(Validate)] for the rules |
| Gates and policies | App::gate, impl Policy, user.authorize(…), .require_gate(…) |
| spatie/laravel-permission | the Permissions module: assign_role, has_permission, .require_role(…) |
| Global scopes (tenancy) | #[model(default_scope = "…")] with renox::context |
| Scout (database engine) | #[model(search = "title, body")] and Post::search(&q) (docs/search.md) |
| Breeze / Jetstream | rnx new --starter: email verification, roles, a dashboard, the users page and the activity log; or Auth::new().account() alone |
| Sanctum | API tokens with abilities (create_token_with, .require_ability(…)) |
Cache::lock | state.cache.lock(name, ttl) |
| Queues, mail, notifications, scheduler | impl Job, mail_view, impl Notification, app.schedule() |
| Horizon | the queue dashboard: .module(renox::queue::Dashboard) |
Http:: facade, Http::fake() | state.http, app.fake_http() |
View::share | App::share |
| Tinker | rnx db:shell and your own commands (App::command) |
Artisan commands ($signature, $this->ask(), confirm(), secret(), choice()) | impl AppCommand on a clap struct, renox::prompt::{ask, confirm, secret, choice} |
@push / @stack | {% call push('scripts') %}…{% endcall %} / {{ stack('scripts') }} |
| Vite + Tailwind | rnx new --tailwind (the standalone CLI, no Node) |
| Livewire | htmx and Alpine.js, with handlers that return fragments |
| Filament tables | renox::grid with renox/grid.html (docs/grid.md) |
| Filament forms, infolists, actions, notifications, widgets | the kit's fields, infolist, action_sheet, Toast and notification_bell, stat/chart(…) with renox::chart (docs/ui.md) |
| Filament's demo app | examples/bikeshop (its back office and admin panel) |
Not planned: runtime-reflected Eloquent-style models, Redis, and a REPL.
The files below are also a documentation site with search, built with Renox itself
(site/): docs.renox.rs, rebuilt whenever they
change on main. The landing page and blog are renox.rs (www/).
rnx new to a server, step by step.Renox 1.0.0 is on crates.io: renox, renox-core, renox-macros, renox-cli, and the
plugins renox-2fa, renox-editors, renox-blocks, renox-oauth, renox-admin and
renox-billing. From here Renox follows semver (docs/stability.md). The Laravel parity review
compares it with Laravel and Filament feature by feature, as of October 2026. Since the first
review (after M17), milestones M18–M32 closed its gaps:
route_is, factory states, plural ranges, #[derive(Validate)], the browser's language,
and a completeness pass (docs for every public item, CLI tests).gt/lt, decimal, dimensions, json, …), route model binding (Found), named
disks, ETags, the XSRF-TOKEN cookie, trusted hosts, and view and redirect routes.current_password, the breach check,
session keep/flash_now, named error bags, several mailers with failover, has_many_through.Then v1.0. It brought:
rnx new --starter) and the 1.x promise;Breaking changes up to 1.0 are listed in CHANGELOG.md.
An app made by rnx from crates.io depends on that release (renox = { version = "…" }); to
upgrade, raise the version and read the changelog. An rnx installed from Git pins its apps
to the commit it was built from (renox = { git = …, rev = "…" }) instead.
Every change is tested in CI on Linux, macOS and Windows, on SQLite and PostgreSQL, against a
real S3 server, with a chaos test, the minimum Rust version, every Cargo feature on its own, and
a new app made with every generator and built into a Docker image. Optional Cargo features:
postgres, s3, uuid, xlsx (and fake, http, server-events, on by default).
Issues and feedback are welcome: see CONTRIBUTING.md, and SECURITY.md to report a vulnerability.
Licensed under either of
at your option.
A batteries-included web framework for Rust, inspired by Laravel: Axum, HTMX and Alpine.js, SQLite or PostgreSQL, deployed as one binary.
Rust
0
617 commits
updated Oct 11, 2026
Laravel's productivity, Rust's performance, one binary to deploy.
A web framework is a toolbox for building websites and web apps, so you don't start from zero. Renox is one for Rust, and it is "batteries included": the pieces most apps need (pages, forms, logins, a database, email, background jobs) come in the box and fit together.
Under the hood, Renox uses well-known parts. Axum is the web server library. HTMX and
Alpine.js are two small scripts that make pages update in the browser without a reload.
SQLite (or PostgreSQL) stores the data. You add one dependency, renox, and you get all of it.
Read the docs at docs.renox.rs, or start at renox.rs. New here? Start with the tutorial: it builds one small app step by step and explains every word. Already know Laravel? Read coming from Laravel; every guide is listed under Documentation. API reference: docs.rs/renox.
rnx build packs your pages
(views), translations, CSS/JS and database changes (migrations) into that one file. Copy it
and a .env settings file to a server, and you're done. You don't need Node, Redis or a
separate program for background jobs. rnx make:deploy writes the Dockerfile, a systemd
unit and Litestream backups for the SQLite file.
The examples/hello guestbook: Rust validation, errors inline, the list swapped in by htmx, and no page reloads.
You need Rust 1.94 or later (rustup installs it) and a C compiler (for
SQLite; on Linux build-essential or your distribution's equivalent, on macOS
xcode-select --install).
cargo install renox-cli # installs `rnx`, Renox's command-line tool
rnx new blog && cd blog # or: --starter, --database postgres, --tailwind
rnx serve # http://127.0.0.1:3000
An app's name uses lowercase letters, digits, - and _, and starts with a letter (blog,
coffee-shop). Rust keywords and the names of crates the app uses (renox, serde, tokio,
std, test, …) are refused, so rnx new MyApp and rnx new test fail.
For the latest main instead of the release: cargo install --locked --git https://github.com/arif-rachim/renox renox-cli.
Then, in the app:
rnx make:module posts --resource --fields "title:string body:text published:bool".
rnx serve rebuilds and restarts by itself, and the browser reloads when a view changes.cargo test. rnx --help lists rnx's own commands (rnx help prints
the same). The app's commands, built-in and your own, are listed by the app itself:
cargo run -- help.The new app has a layout built with the UI kit, Renox's ready-made page parts (navigation bar,
account menu, toasts: small pop-up messages), a home
page, login and registration, an account page (profile, password, other devices), an error page
in the layout, its texts in resources/lang/en.json, a test in tests/home.rs, and an
AGENTS.md and a CLAUDE.md for coding agents. rnx new blog --starter writes the starter kit
on top: a sidebar layout with the notification bell, email verification, roles, a dashboard, a
users page for admins and the activity log, with their tests. To get admin access there, sign
up at /register and run rnx users:admin you@example.com, or run rnx db:seed and log in as
admin@example.com with the password password123. With --database postgres,
create the blog and blog_test databases first (or edit .env).
[!NOTE] The first build compiles every dependency and takes a few minutes. Later builds take seconds. See docs/development.md for faster builds.
A coding agent is an AI assistant that writes code in your project. Renox is made to be easy for them: there is one way to do each thing, generators write the repetitive code (boilerplate), and the docs' code examples are compiled in CI (the automatic checks on every change), so they always match the real code.
In an app made by rnx new, there is nothing to set up. The app has an AGENTS.md (read by
Codex, Cursor, Copilot, Gemini CLI and others) and a CLAUDE.md that imports it (read by Claude
Code). They tell the agent which rnx make:* generators to use, where the cheat sheet, guides
and examples of the Renox version the app uses are, the app's layout, and the traps agents
have hit. Start the agent in the app's folder:
cd blog
claude # or your agent of choice
and ask for what you want, for example:
Add products with a name, a price and a photo: a list with search and pagination, a form that validates, only admins may edit. Use the generators, then write tests and run
cargo test.
In another project, or when the agent doesn't know Renox yet, point it at the docs. Put
this in the project's CLAUDE.md (or AGENTS.md):
This app uses Renox, a Laravel-like Rust web framework (https://github.com/arif-rachim/renox).
Before writing Renox code, read
https://raw.githubusercontent.com/arif-rachim/renox/main/llms.txt: it maps every topic to the
guide or example that shows the official way (its links are paths in that repository).
Prefer `rnx make:*` generators, `use renox::prelude::*`, and the UI kit (`renox/ui.html`) for
pages. Run `cargo test` after changes.
Claude Code, set up once per app (optional, both make it faster):
Give it Renox's docs and examples on disk, at the version your app uses, so it reads them instead of fetching or guessing:
git clone --depth 1 --branch v1.0.0 https://github.com/arif-rachim/renox ~/src/renox
claude --add-dir ~/src/renox # or, inside a session: /add-dir ~/src/renox
Let it build, test and run the generators without asking each time, in the app's
.claude/settings.json (commit it, so the whole team gets it):
{
"permissions": {
"allow": [
"Bash(cargo build:*)",
"Bash(cargo check:*)",
"Bash(cargo clippy:*)",
"Bash(cargo test:*)",
"Bash(cargo fmt:*)",
"Bash(rnx make:module:*)",
"Bash(rnx make:model:*)",
"Bash(rnx make:migration:*)",
"Bash(rnx migrate)",
"Bash(rnx migrate:status)"
]
}
}
name:* allows every command that starts with name, so rnx migrate is listed exactly:
commands that drop data or reach the outside world (rnx migrate:fresh, rnx db:seed,
deploys) are better left to ask.
Tips:
~/.cargo/registry/src/*/renox-core-*/), so
an agent can read the real code instead of guessing.renox::testing::TestApp drives the app without a server,
so cargo test checks pages, forms, mail and jobs in seconds.Here is a small guestbook. A form is checked (validated) in Rust, a new row is saved in the database, and only the updated list is sent back, so htmx swaps it into the page without a reload:
use renox::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Model, Serialize, Default)]
#[model(table = "entries")]
struct Entry { id: i64, name: String, message: String }
#[derive(Deserialize)]
struct EntryForm { name: String, message: String }
impl Validate for EntryForm {
fn rules(&self, v: &mut Validator) {
v.field("name", &self.name).required().max(50);
v.field("message", &self.message).required().between(3, 280);
}
}
// Bad input never gets here: htmx gets a 422 and the errors appear next to the fields.
async fn store(State(db): State<Db>, Valid(form): Valid<EntryForm>) -> Result<View> {
// Save the new entry as a row in the `entries` table.
Entry::create(&db, Entry { name: form.name, message: form.message, ..Default::default() }).await?;
// Read every entry back, newest first.
let entries = Entry::query().latest().get(&db).await?;
// Render only the template's `entries` block; htmx puts it into the page.
Ok(view("guestbook.html", context! { entries }).fragment("entries"))
}
// Routes live in modules, registered with `App::new().module(Guestbook)`.
struct Guestbook;
impl Module for Guestbook {
fn name(&self) -> &'static str { "guestbook" }
fn routes(&self) -> Routes {
Routes::new().post("/entries", store).name("entries.store")
}
}
<form hx-post="/entries" hx-target="#entries" hx-swap="outerHTML"> <!-- CSRF sent for you -->
<input name="name"> <textarea name="message"></textarea> <button>Send</button>
</form>
{% block entries %} {# all htmx gets back #}
<ul id="entries">{% for e in entries %}<li>{{ e.name }}: {{ e.message }}</li>{% endfor %}</ul>
{% endblock %}
Everything below is built in. Click a heading to open it. Words in code are the names you
type in Rust, templates or the terminal.
/products, run this function") are grouped in
modules and in prefixed groups (Routes::group("/admin", "admin.", …)), with names
(route('products.edit', id) in templates), guards that decide who may open them
(.require_auth(), .guest_only(), .require_verified(), .require_gate("admin"),
.require_role(…), .require_permission(…), .require_ability(…) for API tokens,
.require_password_confirmed()) and rate limits that cap how often someone may call them
(.throttle(60, Duration::from_secs(60)), or a named limiter that picks the limit per user or
API key: .throttle_by("api")).Routes::domain("{account}.example.com", …)), a fallback for what
nothing else answers, redirects by route name (Redirect::route, Redirect::intended), and
the current route in views (route_is('admin.*')) and handlers (CurrentRoute).Found(post): Found<Post> loads the row {post}, {id} or {slug}
names (404 if there's none, within the tenant's default scope). Pages without a handler
(.view("/about", "about.html"), .redirect(…)), and .etag() for 304s on feeds and lists.SESSION_DRIVER=database). Flash messages (shown once, on the next
page), old input (what the visitor typed, kept after an error) and list/counter helpers
(session.push, session.increment) come built in. CSRF protection, which stops other sites
from sending forms in your visitors' name, is automatic for forms and htmx._method spoofing lets plain HTML forms, which can only send GET and POST, send PUT and
DELETE.TRUSTED_HOSTS refuses other hosts, and App::xsrf_cookie() hands the CSRF token to
JavaScript clients as Laravel's XSRF-TOKEN cookie.impl Webhook, .webhook::<W>(path)):
signature checks (HMAC, Stripe-style), each event stored and processed once in the queue,
with webhook:retry when something failed.my-app down --secret …) and /health are included.Cookies, SetCookie), downloads and streamed responses
(Download), and abort(StatusCode::GONE, "…") for any status with a message.App::layer,
Routes::route_layer), shared services (App::provide), template filters (App::templates),
data for every view (App::share) and async gates..fragment("block") answers htmx with just one block of the page, not the whole page.
HxTrigger, HxRedirect and Back cover the rest.{{ csrf_field() }}, {{ method_field('PUT') }}, old(), error(), t() and route() work
in every template (route('products.index', q=q) adds a query string), can('update', product) on models the handler wrapped with Can::new, and
pagination(products) once imported from renox/pagination.html.asset('app.css') adds a content hash (?v=…) that changes when the file changes, so
browsers can keep (cache) assets for a year and still get new versions.<meta> into the layout's
{{ stack('scripts') }} / {{ stack('head') }}, once if asked ({% call push('scripts', once='chart') %}).rnx new --tailwind, and rnx serve / rnx build run Tailwind's
standalone CLI (downloaded once, checked by SHA-256).old, error, t, can, auth), and a UI kit
ships with Renox (renox/ui.html), with a warm default theme (Inter and Poppins bundled, a
type scale that puts the important figure first; a classic theme too):
lines[0][qty]) read into a Vec of structs;APP_CURRENCY),
dates, "3 hours ago", badges, Yes/No, swatches, pairs or Markdown;Toast::success(…), with a body, links, a duration and a position) and live
validation.renox::chart): figures with their change and a
sparkline, line, area, bar, pie and doughnut charts drawn on the server as HTML and SVG
(crosshair tooltips, keyboard, a data table, colours checked for colour blindness), values
per day or month from a query (Trend::of(query, "created_at").over(period).sum(…)), a
period filter, and widgets that load on their own and refresh.renox::grid): filters per column by kind (a date range
calendar for dates), server pages and sorting in the URL, grouped headings, frozen columns,
different columns on phones and desktops kept per user, and cells drawn by the page
({{ sparkline(…) }} charts, buttons); columns moved and resized by dragging, details with audit fields, editing in place, rows
dragged into order, merged cells, and CSV/Excel/print exports. Also a search box, filter
chips, rows that link to a page, several grids on one page, bulk and row actions, summaries
and groups, cards on phones, image/badge/link/copyable cells, columns from related tables,
an advanced filter, state remembered in the session and polling (guide)..also("block") sends out-of-band blocks with a fragment; HxRetarget, HxReswap and
HxPushUrl set the other htmx headers.#[derive(DbEnum)]), multi-selects (Json<Vec<_>>), dates, times, JSON, UUIDs
(docs/types.md).migrate, migrate:rollback, migrate:fresh --seed), with per-database files when SQL
differs.#[derive(Model)]
gives it create, save, delete (with optional soft deletes), find_or_404,
a query builder (OR groups, sub-queries, where_has, aggregates, group_by/having, raw
fragments, row locks, bulk updates, upserts, update_or_create, chunks), pagination (numbered,
simple or by cursor) and factories with fake data. The key is the id field's type: a
number the database counts, or a ULID, UUID or string
(rnx make:model Invoice --module invoices --key ulid; the module must exist).save_changes, save_only), run hooks (saving, saved,
deleting, deleted), carry a default scope, e.g. the current tenant, that every query
applies until unscoped(), and keep secrets encrypted at rest (Encrypted<String> fields,
sealed with APP_KEY). Transactions nest with savepoints (tx.savepoint(…)).belongs_to, has_many, many-to-many pivots (with
pivot columns) and polymorphic Morph load a page's related rows in one query each (two for
pivots: the pivot rows, then the models), and
count_many / sum_many give counts and sums per row (guide); joins
read into #[derive(FromRow)] structs with fetch_as.tsvector): #[model(search = "title, body")], a migration from
renox::db::search::migration::<Post>, then Post::search(&q) with any other filter and
pagination; the database keeps the index current, and the data grid's search box uses it
(guide).? placeholders, and transactions that can retry on a
busy database (db.transaction_retrying(3, …)): renox::db::sql("…").bind(x).fetch_all(&db).Valid<T> validates forms,
JSON bodies and query strings with rules such as required,
required_if, email, between, matches (regex), digits, dates (before, after),
unique, exists, same, gt/lt against another field, decimal, alpha_dash, uuid,
json, timezone, size, image, mimes, dimensions (pixels) and current_password, per item of
a list (each, nested, distinct), and your own reusable Rules. Messages come in English,
or from your own translations (resources/lang/<locale>.json). Simple forms declare them as attributes:
#[derive(Validate)] with #[validate(required, email, unique("users", "email"))].prepare tidies the input, authorize answers 403 before any rule, and
after runs checks that need the database, with errors shown like a rule's.Auth::new() adds login, registration, logout, remember me, password reset and email
verification, with Argon2id password hashing (passwords are stored scrambled, never as typed)
and login throttling, on pages built with the UI kit;
.account() adds a profile page
(email change with re-verification, password, "log out other devices", delete account).
Logout ends only this device. Password rules come from Password::min(12).mixed_case()…
(.uncompromised() refuses passwords from known breaches, asking Have I Been Pwned by hash
prefix), and
users imported from Laravel log in with their bcrypt hashes.renox-2fa crate: one module, and users turn on codes
from an authenticator app on their account page, with recovery codes
(guide).LoggedIn, LoginFailed, Registered, …) and an opt-in Audit module that
records them, plus your own entries (audit::record).Authorization: Bearer) with abilities and expiry serve mobile apps and
integrations.user.authorize("update", &product)? in
handlers, can(...) in views, App::gate_before for super-admins, and an opt-in
Permissions module with roles and permissions (user.assign_role(db, "editor"),
.require_role("editor")).queue:failed / queue:retry.
On PostgreSQL, workers on several servers never take the same job. Queues drain in priority
order (--queue high,default); jobs can be unique, encrypted, rate limited or kept from
overlapping, have a failed hook, and run in chains or in batches with progress.every_minutes(5, …), daily_at("02:00", …),
cron("30 9 * * 1-5", …), weekly_on, monthly_on, with weekdays(), between(…),
on_failure(…)). It runs inside serve in APP_TIMEZONE or a task's own IANA zone, daylight
saving included, and each run is claimed once when several servers share the database./_renox/mail while developing. More
mailers by name (App::mailer), and MAIL_FAILOVER to a second provider when the first
is down.t() and components (button, panel, table).
Auth::new().notifications() adds a bell for the navigation bar: unread badge, a panel to
read, mark and delete them, and new ones arriving live over Server-Sent Events./_renox/queue (behind a gate) shows waiting jobs, throughput, failed
jobs to retry or forget, and batch progress.state.http calls other services with timeouts and retries; in tests, app.fake_http()
answers instead and no request reaches the network. Scheduled tasks can ping health checks.storage.list, copy and rename work on both. More disks,
each with a name (App::disk("backups", …), state.disk_named("backups")), sit next to the default.remember, put, forget, add,
pull, increment). It lives in memory or in the database, with atomic locks
(state.cache.lock("stock:42", ttl)) that hold across servers on the database store.App::detect_locale()), from resources/lang/*.json, with t(), plurals and
Laravel's plural ranges. Renox's own texts are English; the same files translate them (ui.*,
renox.auth.*, renox.validation.*).{{ seo(title=…, description=…, image=…) }} writes the title, description, canonical URL,
OpenGraph and Twitter card tags.robots.txt is generated for you, and Sitemap builds sitemap.xml from routes and models.
Staging servers say noindex. /favicon.ico answers a quiet 204 until you add one..env, CSP-ready with nonces, and
only in production.analytics::event(&session, "sign_up", …) reaches gtag with the htmx swap, the page or the
next page. ServerEvent sends from the server through the Measurement Protocol, where ad
blockers can't drop it.use renox::prelude::*;
use renox::testing::TestApp;
#[renox::test]
async fn login_page_and_registration_errors() {
let app = TestApp::new(App::new().module(Auth::new())).await; // fresh DB, fake mail and queue
app.get("/login").await.assert_ok().assert_see("Log in");
app.htmx().post("/register", &[("email", "nope")]).await.assert_invalid("email");
}
rnx make:module products --resource --fields "name:string price:money" # a whole CRUD with tests
rnx make:module news --resource --model Article # when the model's name isn't the module's in the singular
# also make:model -m, make:policy, make:job, make:factory, make:test…
rnx make:command orders:close --module orders # a typed command (clap): --help, checked arguments, prompts
rnx route:list # every route with its name, module and guards
rnx db:shell # SQL prompt, no sqlite3/psql needed
rnx build && rnx make:deploy # dist/blog + Dockerfile, systemd (+ socket), Litestream
rnx make:deploy also writes a systemd socket unit: deploys then restart the app without
refusing a single connection.
In production, timeouts keep a slow database or mail server from holding requests, /health
feeds your load balancer, and panics in handlers, jobs and tasks are contained. CI checks this by
stopping, pausing and locking the database under a running app
(running in production). Tests are covered in docs/testing.md.
Every request gets an id that's in its log lines and its X-Request-Id. Logs can be JSON
(LOG_FORMAT=json) or go to a file. App::report hands 500s, failed jobs and failed tasks to
Sentry or a chat channel. Error pages use the app's layout. While developing,
/_renox/debug shows the last requests with their SQL, and flags N+1 queries.
rnx new include an AGENTS.md (and a CLAUDE.md importing it) that tells an
assistant how the project works; see Use Renox with Claude Code.| Renox | Loco | Axum on its own | |
|---|---|---|---|
| Inspired by | Laravel | Rails | (a library, not a framework) |
| Front end | Server-rendered + htmx + Alpine, bundled, no Node | Server-side templates, or a client-side app built with npm | Up to you |
| Data | Its own light model layer on sqlx; SQLite first, PostgreSQL optional | SeaORM entities | Up to you |
| Jobs | Queue in your database (SQLite or PostgreSQL), run inside the same binary | Queue in Redis, PostgreSQL or SQLite, or in-process tasks | Up to you |
| Auth, mail, uploads, i18n | Built in, with pages and translations | Mailers and storage built in; auth comes with the SaaS starter | Assemble from crates |
| Deploy | One binary with its assets + .env; Dockerfile, systemd, Litestream generated | Binary + config; Dockerfile, nginx or AWS Lambda generated | Up to you |
| Maturity | 1.0 on crates.io, one maintainer | Released on crates.io, larger community | Mature, widely used |
Choose Loco if you prefer Rails conventions, SeaORM or a JavaScript front end. Choose Axum on its own if you want to assemble every piece yourself. Choose Renox if you want Laravel's everything-included workflow and HTML over the wire, deployed as a single file.
Two examples, both on the UI kit:
examples/bikeshop: the one complete use case. A bike shop with
three stores that sell, rent and service bikes, on real-sized data. It has:
/about/fields (every input type ↔ Rust ↔ SQLite ↔ PostgreSQL) and /about/htmx
(every htmx recipe, live);Every page explains itself beside its own code: from 1200 px, "About this page" is docked next to the page with the handler, template and queries behind it, cut from the source at build time. Live at bikeshop.renox.rs. Start here.
examples/hello: the smallest app, the guestbook from the GIF. It has an
HTMX form, a photo upload, an event that queues mail, a scheduled task, English and Spanish
texts, login and an account page.
| Laravel | Renox |
|---|---|
php artisan | rnx (rnx make:model, rnx migrate, rnx route:list, …) |
Route::resource, make:controller --resource | Routes::resource, rnx make:module --resource |
Event::fake, Notification::fake, $this->travel() | app.fake_events(), app.fake_notifications(), app.travel(…) |
| Blade | MiniJinja templates, with {% extends %} and {% block %} |
| Blade components, Breeze's UI | Macros that see the request (rnx make:component), the renox/ui.html kit |
routes/web.php, Route::prefix()->name()->group() | Module::routes, Routes::group("/admin", "admin.", …) |
Route::domain, Route::fallback, redirect()->route(), ->intended() | Routes::domain(…), Routes::fallback(…), Redirect::route(…), Redirect::intended(…) |
HasUlids / HasUuids, encrypted casts, nested transactions | id: Ulid / id: Uuid, Encrypted<T> fields, tx.savepoint(…) |
routeIs, @class, trans_choice ranges | route_is('admin.*'), class_names(…), {0} none|[1,*] :count in lang files |
| Factory states and sequences | Product::factory().count(3).state(f).sequence(|i, p| …) |
Route model binding, Route::view, Route::redirect | Found<Post>, .view(…), .redirect(…) |
Several disks (Storage::disk('s3')) | App::disk(name, …), state.disk_named(name) |
Several mailers, the failover transport | App::mailer(name, …), state.mailer_named(name), MAIL_FAILOVER |
hasManyThrough, named error bags | relations::has_many_through, #[validate(bag = "login")] |
| Middleware | .require_auth(), .throttle(…), Routes::route_layer, App::layer |
RateLimiter::for('api', …) | App::rate_limiter("api", …) and .throttle_by("api") |
Exception reporting (report()), Telescope/Debugbar | App::report(…), /_renox/debug |
| Eloquent | #[derive(Model)] and the query builder; relations are explicit loaders (docs/relations.md) |
Form Requests (authorize, prepareForValidation, after) | Valid<T> with impl Validate (authorize, prepare, after), or #[derive(Validate)] for the rules |
| Gates and policies | App::gate, impl Policy, user.authorize(…), .require_gate(…) |
| spatie/laravel-permission | the Permissions module: assign_role, has_permission, .require_role(…) |
| Global scopes (tenancy) | #[model(default_scope = "…")] with renox::context |
| Scout (database engine) | #[model(search = "title, body")] and Post::search(&q) (docs/search.md) |
| Breeze / Jetstream | rnx new --starter: email verification, roles, a dashboard, the users page and the activity log; or Auth::new().account() alone |
| Sanctum | API tokens with abilities (create_token_with, .require_ability(…)) |
Cache::lock | state.cache.lock(name, ttl) |
| Queues, mail, notifications, scheduler | impl Job, mail_view, impl Notification, app.schedule() |
| Horizon | the queue dashboard: .module(renox::queue::Dashboard) |
Http:: facade, Http::fake() | state.http, app.fake_http() |
View::share | App::share |
| Tinker | rnx db:shell and your own commands (App::command) |
Artisan commands ($signature, $this->ask(), confirm(), secret(), choice()) | impl AppCommand on a clap struct, renox::prompt::{ask, confirm, secret, choice} |
@push / @stack | {% call push('scripts') %}…{% endcall %} / {{ stack('scripts') }} |
| Vite + Tailwind | rnx new --tailwind (the standalone CLI, no Node) |
| Livewire | htmx and Alpine.js, with handlers that return fragments |
| Filament tables | renox::grid with renox/grid.html (docs/grid.md) |
| Filament forms, infolists, actions, notifications, widgets | the kit's fields, infolist, action_sheet, Toast and notification_bell, stat/chart(…) with renox::chart (docs/ui.md) |
| Filament's demo app | examples/bikeshop (its back office and admin panel) |
Not planned: runtime-reflected Eloquent-style models, Redis, and a REPL.
The files below are also a documentation site with search, built with Renox itself
(site/): docs.renox.rs, rebuilt whenever they
change on main. The landing page and blog are renox.rs (www/).
rnx new to a server, step by step.Renox 1.0.0 is on crates.io: renox, renox-core, renox-macros, renox-cli, and the
plugins renox-2fa, renox-editors, renox-blocks, renox-oauth, renox-admin and
renox-billing. From here Renox follows semver (docs/stability.md). The Laravel parity review
compares it with Laravel and Filament feature by feature, as of October 2026. Since the first
review (after M17), milestones M18–M32 closed its gaps:
route_is, factory states, plural ranges, #[derive(Validate)], the browser's language,
and a completeness pass (docs for every public item, CLI tests).gt/lt, decimal, dimensions, json, …), route model binding (Found), named
disks, ETags, the XSRF-TOKEN cookie, trusted hosts, and view and redirect routes.current_password, the breach check,
session keep/flash_now, named error bags, several mailers with failover, has_many_through.Then v1.0. It brought:
rnx new --starter) and the 1.x promise;Breaking changes up to 1.0 are listed in CHANGELOG.md.
An app made by rnx from crates.io depends on that release (renox = { version = "…" }); to
upgrade, raise the version and read the changelog. An rnx installed from Git pins its apps
to the commit it was built from (renox = { git = …, rev = "…" }) instead.
Every change is tested in CI on Linux, macOS and Windows, on SQLite and PostgreSQL, against a
real S3 server, with a chaos test, the minimum Rust version, every Cargo feature on its own, and
a new app made with every generator and built into a Docker image. Optional Cargo features:
postgres, s3, uuid, xlsx (and fake, http, server-events, on by default).
Issues and feedback are welcome: see CONTRIBUTING.md, and SECURITY.md to report a vulnerability.
Licensed under either of
at your option.