adminx
One Resource definition → a complete admin panel. Any web framework, any database.
A framework-neutral admin-panel framework for Rust. Write a resource once and serve it over Actix Web or Axum, backed by PostgreSQL · MySQL · SQLite (SeaORM) or MongoDB. The logic lives in a neutral core; the frameworks and databases are thin, swappable adapters — switching either is a one-line change and your resource code never moves.
Per resource, with almost no boilerplate, you get:
- 🧩 Auto CRUD — a REST/JSON API and a rendered HTML admin UI
- 🔍 List filters — text, select, boolean, and date-range (collapsible sidebar)
- 🔐 Auth + RBAC — JWT-in-cookie login and role-gated routes, plus opt-in per-action RBAC (
adminx-rbac): DB-backed permissions, editable right in the panel - 🛡️ Hardened by default — CSRF tokens on every form, per-account rate limiting on login & MFA
- 🔒 MFA — TOTP (authenticator apps) with one-time backup codes
- ⚡ Custom actions, CSV/JSON export, pagination, sorting, soft-delete
- 🌱 Seeding and admin-user creation from a CLI or from code
- 🔄 The same resource code on Actix or Axum, over SQL or Mongo
Contents
- Install
- How to use
- Examples
- How to seed
- Implement on every stack
- Usage examples
- Full example (single file)
- Reference
- The
Resourcetrait - Customizing the form
- Filters
- Custom actions
- Authentication & RBAC
- CSRF protection
- Rate limiting
- Fine-grained RBAC (
adminx-rbac) - Protecting the panel with HTTP Basic Auth
- Multi-factor auth (MFA)
- The
adminxCLI - Admin-users table
- Managing admin users
- CSV / JSON export
- REST API surface
- HTML admin UI
- Environment variables
- Deployment
- Troubleshooting
- Status
- The
Install
Depend on the single adminx facade and pick one framework (actix or
axum) and one storage (seaorm or mongo) via features. Crates you don't
select are never compiled.
[]
= { = "3", = ["axum", "seaorm"] }
= { = "1", = ["full"] }
= "0.8" # the framework you chose
= "1"
= "0.1"
| You want | features = [...] |
web dep |
|---|---|---|
| Axum + Postgres/MySQL/SQLite | ["axum", "seaorm"] |
axum = "0.8" |
| Actix + Postgres/MySQL/SQLite | ["actix", "seaorm"] |
actix-web = "4" |
| Axum + MongoDB | ["axum", "mongo"] |
axum = "0.8" |
| Actix + MongoDB | ["actix", "mongo"] |
actix-web = "4" |
You do not add
sea-ormormongodbyourself — the storage adapter wraps the driver. During local development use a path dep:adminx = { path = "../crates/adminx-suite/adminx", features = [...] }.
How to use
Three steps: define a resource, wire a main, open the panel.
1. Define a resource
A resource maps a table/collection to an admin screen. Four methods are required; everything else has a default.
use *;
use async_trait;
;
That already gives you a list view, detail page, create/edit forms, a JSON API, CSV/JSON export, and role-gated auth — all generated.
2. Wire main (Axum + SQLite here)
Put this in the same src/main.rs as the PostResource above. Cargo deps:
adminx = { version = "3", features = ["axum", "seaorm"] }, plus tokio
(features = ["full"]), axum = "0.8", async-trait, serde_json. SQLite needs
no database server, so this runs as-is with cargo run.
use *;
async
3. Open the panel
Visit http://localhost:8080/adminx and sign in with admin@example.com /
changeme. First login prompts the (skippable) MFA setup.
Auth is optional: until
configure_auth(..)is called, every page is public — handy for a quick look.
Examples
The same Resource trait scales from one line to a fully customized screen.
Minimal — one text input per permitted column:
Custom form + sidebar grouping:
With list filters (adds the collapsible filter sidebar — see Filters):
Role-gated — only these roles can reach it:
Mongo — collections are schemaless, and the key is _id:
// collection
// <-- Mongo only
How to seed
Populate tables/collections with starter data. Three ways, all idempotent when
you write your statements that way (ON CONFLICT DO NOTHING, etc.).
From the CLI (recommended)
Install the CLI once, then seed by pointing DATABASE_URL (SQL) or MONGO_URL
(Mongo) at your database. One statement per line; blank lines and -- / #
comments are ignored.
SQL (seeds.sql):
INSERT INTO categories (name, slug) VALUES ('Books','books') ON CONFLICT (slug) DO NOTHING
INSERT INTO products (name, sku, price_cents, active) VALUES ('Rust Book','SKU-RB',3999,true) ON CONFLICT (sku) DO NOTHING
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/mydb
Mongo — each line is a JSON command document (seeds.json):
MONGO_URL=mongodb://127.0.0.1:27017 MONGO_DB=mydb
# or pipe it
| MONGO_URL=...
From code, per backend
// SeaORM — SQL statements. Connects and runs them; returns rows affected.
seed.await?;
// Mongo — JSON command documents.
seed.await?;
From code, after set_storage (backend-neutral)
Once a backend is registered, adminx::seed runs against whichever one is active
— SQL strings on SeaORM, JSON command docs on Mongo:
seed.await?;
adminx create-admin(below) seeds the admin user the same way — from the CLI orcreate_admin(email, password, role)in code.
Implement on every stack
The Resource is identical across all four combos — only main changes (and,
for Mongo, primary_key() -> "_id"). Select the stack with Cargo features from
Install.
Axum + SeaORM
features = ["axum", "seaorm"], dep axum = "0.8".
use *;
async
Swap the URL for another SQL dialect — postgres://…, mysql://…, or
sqlite://file.db?mode=rwc. If your tables already exist, skip execute_sql and
use adminx::seaorm::init(url).await? (connect + register in one call).
Actix + SeaORM
features = ["actix", "seaorm"], dep actix-web = "4".
use *;
async
Axum + MongoDB
features = ["axum", "mongo"]. Mongo is schemaless (no DDL), key is _id:
use *;
async
Actix + MongoDB
features = ["actix", "mongo"]. Combine the Actix main (above) with Mongo setup:
use *;
async
Runnable references live in
projects/demos/axumtestsqlandprojects/demos/actixtestsql(SQL-only, self-contained), plusadminx-demo(Axum + SQLite).
Usage examples
Create an admin user (CLI — backend chosen from the environment):
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/mydb \
EMAIL=admin@example.com PASSWORD=changeme
MONGO_URL=mongodb://127.0.0.1:27017 MONGO_DB=mydb \
EMAIL=admin@example.com PASSWORD=changeme
Filter the list (query string mirrors the UI sidebar):
/adminx/products/list?name=rust # text contains
/adminx/products/list?active=false # boolean
/adminx/products/list?created_at_from=2024-01-01&created_at_to=2024-12-31
Export the current (optionally filtered) list:
/adminx/products/list?download=csv
/adminx/products/list?download=json&active=true
Use the JSON API (relative to /adminx):
&per_page=25&sort=-created_at # list
Run a custom action on a record:
POST /adminx/orders/{id}/action/refund
Full example (single file)
A complete, copy-paste project — Axum + SQLite, with a filtered resource,
seeded rows, and auth. No database server needed: cargo run, then open
http://localhost:8080/adminx and sign in with admin@example.com / changeme.
Cargo.toml:
[]
= "adminx-quickstart"
= "0.1.0"
= "2021"
[]
= { = "3", = ["axum", "seaorm"] }
= { = "1", = ["full"] }
= "0.8"
= "0.1"
= "1"
src/main.rs:
use *;
use async_trait;
;
async
Swap two lines to move to production: change the connect URL to
postgres://… (and the CREATE TABLE to Postgres DDL, e.g. SERIAL PRIMARY KEY),
or to Mongo with adminx::mongo::init(uri, db) and primary_key() -> "_id".
Reference
The Resource trait
Four methods are required; the rest have defaults you override to customize.
| Method | Required | Default / purpose |
|---|---|---|
resource_name() |
✅ | display name (e.g. "Posts") |
base_path() |
✅ | URL segment (e.g. "posts") |
table_name() |
✅ | SQL table / Mongo collection |
clone_box() |
✅ | Box::new(self.clone()) |
primary_key() |
"id" (use "_id" for Mongo) |
|
permit_keys() |
[] — columns settable on create/update |
|
readonly_keys() |
["id","created_at","updated_at"] |
|
allowed_roles() |
["admin"] — RBAC gate |
|
menu_group() / menu() |
sidebar grouping / label | |
form_structure() |
custom form (else derived from permit_keys) |
|
filterable_fields() |
[] — list filters (see below) |
|
custom_actions() |
[] — id-scoped actions |
|
soft_delete() |
true when "deleted" is permitted |
|
list/get/create/update/delete |
full default CRUD via Storage |
|
list_page/new_page/edit_page/view_page |
full default HTML pages |
Overrides return the neutral ApiResponse, so they keep working on both frameworks.
Customizing the form
Without form_structure(), the create/edit form is one text input per
permit_keys(). Provide one to control labels and field types:
Field types: text, number, email, password, textarea, checkbox (any
HTML input type works for the plain case).
Filters
Declare filterable_fields() and adminx renders a collapsible filter sidebar
on the list page (hidden by default; a "Filters" button toggles it, and it opens
automatically when a filter is active). Filters apply on both storage backends and
also constrain CSV/JSON export.
| Kind | Match | Query params |
|---|---|---|
text |
case-insensitive substring | ?field=value |
select / boolean |
exact | ?field=value |
date_range |
>= from AND <= to |
?field_from=YYYY-MM-DD&field_to=YYYY-MM-DD |
A bare to date (YYYY-MM-DD) covers the whole day (extended to 23:59:59).
FilterField / FilterKind / FilterOption come from the prelude. On Mongo,
contains becomes a case-insensitive $regex and a date range becomes
{$gte,$lte}.
Custom actions
Id-scoped buttons on the detail page that POST to /{base}/{id}/action/{name}.
Declare them by returning CustomActions from custom_actions(), each with its
own async handler (CustomAction / ActionFuture are in the prelude); see
adminx-core/src/actions.rs.
Authentication & RBAC
adminx has built-in login: a signed JWT (HS256) in an HttpOnly cookie — no server-side session, so it behaves identically on Actix and Axum. Set it up in four steps.
1. Create the admin table (see Admin-users table), or
let adminx create-admin create it for you on SeaORM.
2. Configure auth once at startup:
configure_auth;
| Field | Meaning |
|---|---|
jwt_secret |
HS256 signing key — keep it secret; rotating it invalidates all sessions |
token_ttl_secs |
how long a login stays valid, in seconds |
admin_table |
where admin users live |
secure_cookie |
true in production (HTTPS); false for local http:// |
3. Seed an admin (once) — in code or via the CLI:
create_admin.await?; // bcrypt-hashed
4. Log in. adminx adds GET/POST /adminx/login and GET /adminx/logout, and
enforces access automatically:
- Unauthenticated UI page →
303redirect to/adminx/login. - Unauthenticated API request →
401. - A resource is reachable only by principals holding one of its
allowed_roles().
Sessions & revocation. The session is a stateless signed JWT — there's no server-side session store, so
/adminx/logoutclears the browser's cookie but can't invalidate a token that was already copied elsewhere; that token stays valid until it expires. Two levers control the blast radius:
token_ttl_secsbounds how long any leaked token lives — set it short (e.g.3_600) if that matters more than staying logged in.- Rotating
jwt_secretinvalidates every session immediately — the built-in "revoke all" / "log everyone out" switch. (Per-user revocation would need a stateful session store; not built in.)
Don't leave auth off in production. Until
configure_auth(..)is called every route is public and RBAC is bypassed — adminx logs a loud one-time warning the first time a request hits an access check with auth unconfigured.
Roles (RBAC). Each resource declares who may open it; the role comes from the
admin user's role column and travels in the JWT:
Auth is opt-in. Until
configure_auth(..)is called, every page is public — handy while prototyping. Call it (and seed an admin) to lock things down.
CSRF protection
Every built-in form post is protected by a double-submit cookie: a
random token is set in an adminx_csrf cookie and mirrored in a hidden _csrf
field, and the two must match or the post is rejected with a 403. This covers
the auth forms (/login, /mfa/enable, /mfa/verify) and the resource
forms (create, update, delete, and custom-action buttons). It's automatic — the
built-in templates already include the field.
Resource-form CSRF follows the same opt-in rule as the rest of auth: it's
enforced once configure_auth(..) is called, and skipped while auth is
unconfigured (the prototype mode where every page is public anyway). The auth
forms are always checked.
The auth cookie is already SameSite=Strict, which by itself blocks forged posts
to authenticated endpoints. The token matters most for POST /login, which
carries no prior cookie and so gets no SameSite protection: without it an
attacker can force a victim's browser to log into the attacker's account. On
the already-authenticated resource forms the token is defence in depth — legacy
browsers, and same-site-but-not-same-origin attackers (a hostile subdomain is
"same-site").
If you render your own form against any of these routes, add the token:
Custom-action bodies (behaviour change). The
/{id}/action/{name}route now reads a url-encoded form body, matching what the action button always sent (previously the button's body was parsed as JSON, so it silently arrived empty). Acustom_actions()handler now receives its submitted fields as the JSONbody— an added<input name="reason">arrives as{"reason": "..."}. If you were POSTing to this endpoint directly with a JSON body, switch to form encoding.
Rate limiting
Failed password and second-factor attempts are throttled per account, on by
default — a 6-digit TOTP is only a million combinations, so the throttle is
what makes the second factor worth having. Exceeding a limit returns 429 until
the window lapses; a successful login clears the count.
| Default | Meaning | |
|---|---|---|
login |
10 per 15 min | failed password attempts per account |
mfa |
5 per 15 min | failed TOTP/backup-code attempts per account |
Tune it (before serving — the first attempt locks the defaults in):
use ;
configure;
Two things to know about the design:
- Counters are per-process, held in memory. N replicas behind a load balancer therefore allow N× the attempts. That still bounds the attack, but a hard global limit needs a shared store (see ROADMAP).
- Keyed by account, not by client address. This stops a targeted brute force against one admin, but not credential stuffing spread across many accounts. The flip side is that an attacker can deliberately burn an admin's attempts to keep them out — which is why this is a short self-clearing window rather than a lockout needing manual intervention.
Fine-grained RBAC (adminx-rbac)
The built-in check is coarse: a resource lists allowed_roles() and every
operation on it shares that list. For per-action control — "editors may update
posts but not delete them" — add the optional adminx-rbac crate. It's DB-backed
and editable at runtime, in the spirit of ActiveAdmin's authorization adapter.
= { = "3", = ["axum", "seaorm", "rbac"] }
Grants are (role, action, resource) rows in an adminx_permissions table.
Actions are list / read / create / update / delete / export and each
custom action by name; * is any resource and manage is any action. Declare a
starting policy in code — it seeds the DB on first boot, then the database is
authoritative and admins edit it in the panel (no redeploy):
use ;
init.await?; // 1. storage
seed.await?; // 2. tables (SQL backends only; Mongo skips this)
init.await?;
configure_auth; // 4. turn auth on
register_resource;
register_resources; // 5. in-panel role/permission editors (optional)
How it fits together:
- Pluggable, no core dependency.
adminx-rbacimplements anAuthorizertrait inadminx-coreand registers it. Without the crate, adminx uses the built-inallowed_roles()list exactly as before — RBAC is purely additive. - Storage-agnostic. Grants are read/written through the same
Storagetrait as everything else, so it works over SeaORM or Mongo. The one asymmetry is table creation: SQL needsrbac::migrate_sql()(SQLite-flavoured — adapt the DDL for Postgres/MySQL), Mongo needs nothing. - Fast checks. Grants load into an in-memory cache once; the per-request decision does no I/O. A permission edit in the panel reloads the cache.
- Single-writer caching (v1). The cache reloads on writes made through this process, so a multi-instance deployment editing permissions on one node won't invalidate its peers until they reload. A shared-store/TTL option is on the ROADMAP.
Protecting the panel with HTTP Basic Auth
The JWT login above is the main gate. If you also want a coarse HTTP Basic prompt in front of the whole panel — e.g. to hide a staging deployment behind a browser username/password — wrap the mounted routes with framework middleware and read the credentials from an env var so it's easy to toggle.
Axum (add base64 = "0.22"):
use ;
use ;
async
// Gate only the panel:
let panel = router.route_layer;
let app = new.nest;
Actix — do the same header check in a Transform middleware and wrap the scope:
App::new().service(adminx::actix::scope().wrap(YourBasicAuth)).
Multi-factor auth (MFA)
adminx ships TOTP two-factor auth (Google Authenticator, Authy, 1Password, …)
on top of the password login. No extra config — just the three mfa_* columns in
the admin-users table.
The JWT carries an MFA step — ok or pending (a pending session can reach only
the MFA pages):
┌─ mfa_enabled = false ─→ /adminx/mfa/setup (skippable prompt)
POST /adminx/login ───►┤
(password ok) └─ mfa_enabled = true ─→ /adminx/mfa/verify (enforced)
-
Not enabled → logged in, but nudged to
/adminx/mfa/setup(QR + secret). Confirming a code enables MFA and shows 10 one-time backup codes once. The prompt is skippable. -
Enabled → login yields a
pendingsession that must submit a TOTP or a backup code at/adminx/mfa/verify; a used backup code is consumed. -
Details: SHA-1 / 6 digits / 30 s step / ±1 skew; backup codes stored bcrypt-hashed; tokens issued before MFA existed decode as
ok(backward compatible).
The adminx CLI
| Command | Purpose |
|---|---|
adminx create-admin |
Create an admin user (idempotent). Flags -e/--email, -p/--password, -r/--role, or env EMAIL/PASSWORD/ROLE. |
adminx seed --file <path> |
Run seed statements (SQL for SeaORM, JSON commands for Mongo). Reads stdin if --file is omitted. |
Backend is chosen from the environment: DATABASE_URL → SeaORM, or
MONGO_URL + MONGO_DB → Mongo. SeaORM create-admin auto-creates the
adminx_users table (with MFA columns) if missing.
Admin-users table
The admin table/collection needs id, email, encrypted_password (bcrypt),
role, plus three columns for MFA:
(
id SERIAL PRIMARY KEY, -- INTEGER AUTOINCREMENT on SQLite
email TEXT NOT NULL UNIQUE,
encrypted_password TEXT NOT NULL, -- bcrypt hash
role TEXT NOT NULL DEFAULT 'admin',
mfa_enabled BOOLEAN NOT NULL DEFAULT false, -- 0 on SQLite
mfa_secret TEXT, -- base32 TOTP secret
mfa_backup_codes TEXT -- JSON array of bcrypt-hashed codes
);
Already have the table? Add the MFA columns without recreating it:
adminx_users
ADD COLUMN IF NOT EXISTS mfa_enabled BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS mfa_secret TEXT,
ADD COLUMN IF NOT EXISTS mfa_backup_codes TEXT;
On Mongo the collection is schemaless — no DDL needed.
Managing admin users
create_admin (and adminx create-admin) is idempotent — it skips an email
that already exists. To change something afterwards, run raw statements against the
live database with adminx seed (SQL, or Mongo command docs).
Reset a password — delete the user, then recreate (passwords are bcrypt-hashed,
so a plain SQL UPDATE can't set one):
# SQL
| DATABASE_URL=postgres://user:pass@host/db
DATABASE_URL=postgres://user:pass@host/db \
EMAIL=admin@example.com PASSWORD=newpass
# Mongo
| MONGO_URL=mongodb://host:27017 MONGO_DB=db
MONGO_URL=mongodb://host:27017 MONGO_DB=db \
EMAIL=admin@example.com PASSWORD=newpass
Change a role (plain column, no hashing):
| DATABASE_URL=...
Locked out by MFA? Clear the user's MFA to force the setup prompt again:
# SQL
| DATABASE_URL=...
# Mongo
| MONGO_URL=... MONGO_DB=...
CSV / JSON export
Every list exports without extra code (and honours active filters):
GET /adminx/{base}/list?download=csvGET /adminx/{base}/list?download=json
Also available as buttons on the list page. Capped at 10,000 rows; CSV values are RFC-escaped.
REST API surface
Per resource, relative to the mount (/adminx):
| Route | Method | Purpose |
|---|---|---|
/{base}/api |
GET | List — ?page=, ?per_page= (≤200), ?sort=col / ?sort=-col, plus filters |
/{base}/api |
POST | Create (JSON body) |
/{base}/api/{id} |
GET / PUT / DELETE | Get / update / delete |
/{base}/{id}/action/{name} |
POST | Custom action |
/health |
GET | DB connectivity probe |
HTML admin UI
Every resource also gets a Tera-rendered UI (dark-mode aware, TailwindCSS), served identically by both adapters. Record data is autoescaped (XSS-safe).
| Route | Purpose |
|---|---|
/ |
Dashboard (auto menu of registered resources) |
/{base}/list |
Table + pagination + filters + View/Edit/Delete + Export |
/{base}/new, /{base}/edit/{id} |
Create / edit form |
/{base}/view/{id} |
Record detail + custom-action buttons |
/login, /logout, /mfa/setup, /mfa/verify |
Auth + MFA pages |
Environment variables
Conventions used by the demos and the CLI; your app decides what to read.
| Var | Purpose |
|---|---|
DATABASE_URL |
SeaORM URL: postgres://…, mysql://…, sqlite://file.db?mode=rwc |
MONGO_URL + MONGO_DB |
MongoDB connection + database |
JWT_SECRET |
HS256 signing key (openssl rand -hex 32) |
PORT |
listen port |
EMAIL / PASSWORD / ROLE |
inputs for adminx create-admin |
ADMINX_EMAIL / ADMINX_PASSWORD |
seeded admin (demo convention) |
ADMINX_SECURE_COOKIE |
1 when served over HTTPS |
ADMINX_TAILWIND_SRC |
override the UI stylesheet source (see below) |
Self-hosting the UI stylesheet. The admin UI loads Tailwind from
cdn.tailwindcss.com by default — convenient, but a dev-mode build that needs
network access and is blocked by a strict Content-Security-Policy. Point it at a
self-hosted build instead, either with the ADMINX_TAILWIND_SRC env var or in
code:
set_tailwind_src; // served by your app
Deployment
adminx compiles into your app's single binary. Ship that binary, run it under
systemd, and put nginx in front for TLS. Set secure_cookie: true and a
strong JWT_SECRET in production.
1. Build the release binary (locally or on the server):
2. Place the binary + environment on the server:
Your
mainreads these — e.g. setAuthConfig { secure_cookie: true, .. }in production so the session cookie is HTTPS-only.
3. systemd unit — /etc/systemd/system/myapp.service:
[Unit]
Description=My adminx app
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
WorkingDirectory=/opt/myapp
EnvironmentFile=/opt/myapp/app.env
ExecStart=/opt/myapp/myapp
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
4. nginx reverse proxy + TLS — /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name admin.example.com;
return 301 https://$host$request_uri; # force HTTPS
}
server {
listen 443 ssl;
server_name admin.example.com;
ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # tells the app it's on TLS
}
}
&&
Your panel is now at https://admin.example.com/adminx. Update after a new build
with cargo build --release, copy the binary, and sudo systemctl restart myapp.
Hardening tips: keep the app bound to 127.0.0.1 (only nginx faces the
internet), add an HTTP Basic gate or
an nginx allow/deny IP allow-list for staging, and rotate JWT_SECRET to
invalidate all sessions. adminx-demo has a fuller AWS EC2 walkthrough in
adminx-demo/README.md.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| "Invalid email or password" with the right password | The admin was created in a different database than the app reads. Match DATABASE_URL / MONGO_DB to the running app. |
Login loops or lands on /adminx/mfa/verify and you're stuck |
MFA is on and the authenticator is lost — clear it via SQL/Mongo (see Managing admin users). |
| Every page is public, never asks to log in | configure_auth(..) wasn't called — auth is opt-in. |
Login "succeeds" but bounces back to /login |
secure_cookie: true while on plain http:// — the browser drops the cookie. Use secure_cookie: false for local http. |
| Login form returns 403 "Your session expired" | The adminx_csrf cookie didn't reach the POST. Same secure_cookie mismatch as above, or the form was posted from a page served on a different site. Reload /adminx/login and retry. |
| Login returns 429 "Too many attempts" | The per-account throttle tripped (default 10 failures / 15 min). Wait for the window to lapse, or loosen it via ratelimit::configure — see Rate limiting. |
| Mongo: View/Edit links or updates target the wrong record | Add fn primary_key(&self) -> &'static str { "_id" } to the resource. |
| SeaORM: "relation … does not exist" | The table wasn't created — run your CREATE TABLE/seed. adminx doesn't migrate your app tables. |
Publish: "no matching package named adminx-core" |
Publish dependencies first: adminx-core → adapters → adminx. Each must be on crates.io before the next resolves. |
Status
Complete and tested: neutral core, SeaORM (PostgreSQL/MySQL/SQLite) + MongoDB,
Actix + Axum (Axum 0.8), dynamic JSON CRUD, Tera HTML UI, JWT-cookie auth + RBAC,
TOTP MFA with backup codes, list filters, custom actions, CSV/JSON export,
seeding + admin CLI, pagination/sort, health, and the single-name adminx
facade.
Roadmap: a switch to make MFA mandatory (today it's a skippable prompt), and backup-code regeneration from an account page.
🌟 Community
Join our growing community of Rust developers building admin panels with AdminX!
- 📖 Documentation
- 💬 Discussions
- 🐛 Issues
- 📧 Email: info@srotas.space
- 📧 Email: snmmaurya@gmail.com
- 📧 Email: deepxmaurya@gmail.com
📄 License
This project is licensed under the MIT License — see the LICENSE file for details.
🙏 Acknowledgments
- Web frameworks: Actix Web and Axum
- Storage: SeaORM (PostgreSQL / MySQL / SQLite) and MongoDB
- UI: TailwindCSS styling with Tera templates
- Auth: JWT via jsonwebtoken, TOTP via totp-rs
🗺️ Roadmap
We are actively building AdminX step by step.
The roadmap includes phases like core CRUD foundation, extended resource features, authentication & RBAC, export/import, custom pages, UI themes, and optional extensions.
👉 See the full roadmap here: ROADMAP.md
📦 Sample starter template
Made with ❤️ by Srotas Space
👥 Contributors
-
Snm Maurya - Creator & Lead Developer LinkedIn
-
Deepak Maurya - Core Developer & Contributor LinkedIn