Comparison
Runique next to Loco, Cot and Django, its known limits and what's coming.
Contents
Runique / Loco
| Topic | Runique 4.0 | Loco 1.3.1 |
|---|---|---|
| Inspiration | Django | Rails |
| Server | Axum | Axum |
| Database | SeaORM: SQLite, PostgreSQL, MySQL/MariaDB, one engine per build | SeaORM: SQLite, PostgreSQL |
| Migrations | model!{}, makemigrations, migration up/down/status | cargo loco generate model/migration, db migrate/down/status |
| Views | Tera | Tera, replaceable engine |
| Forms | #[form]: validation and HTML rendering | Extraction and validation with validator |
| Administration | Built in, rights per account | Up to the project |
| Authentication | Login, sessions, password reset | JWT, sessions, OAuth, reset in generated applications |
| Web security | CSRF always on, headers and CSP by default; hosts and rate limit to declare | secure_headers to enable (presets including CSP); rate limit through a tower layer |
| Background jobs | Up to the project | Workers, scheduler |
| Cache and storage | External connection registered by the project; local media | Memory and Redis cache; S3, GCP, Azure |
| Generators | Project and migrations | Model, controller, scaffold, worker, mailer, task, deployment |
| License | MIT | Apache-2.0 |
Checked on November 5, 2026 in the documentation and the Loco repository.
Runique / Cot
| Topic | Runique 4.0 | Cot 0.7 |
|---|---|---|
| Inspiration | Django | Django |
| Server | Axum | hyper and tower |
| Database | SeaORM: one engine per build | Own ORM (sea-query, sqlx): SQLite, PostgreSQL, MySQL |
| Migrations | makemigrations, migration up/down/status | cot migration make, applied at startup, migration rollback |
| Views | Tera | Askama |
| Forms | #[form] | #[derive(Form)], file fields |
| Administration | ModelAdmin, rights per account | Built in (AdminModel), access by login |
| Authentication | Login, sessions, password reset | DatabaseUser, sessions |
| Sessions | Memory with database fallback, custom store | Memory, database, file, Redis |
| Web security | CSRF always on, headers and CSP by default; hosts and rate limit to declare | Configurable SameSite; the rest up to the project |
| API | Up to the project | Optional OpenAPI and Swagger |
| Email and cache | Built-in email; cache up to the project | Email; memory and Redis cache |
| Tests | runique test, transactional isolation | cot::test, end-to-end (e2e_test) |
| License | MIT | MIT or Apache-2.0 |
Checked on November 5, 2026 in the guide and the Cot repository.
Runique / Django
CLI
Real commands of the runique binary (see runique/src/bin/runique.rs): new, create-superuser, test, makemigrations, migration up. There is no runique start — admin is plain Rust, cargo run is enough. Rolling back / listing migrations goes through SeaORM, not a Runique subcommand.
| Command | Django | Runique |
|---|---|---|
| Create project | django-admin startproject name | runique new name |
| Create app | python manage.py startapp name | — |
| Migrations (generate) | python manage.py makemigrations | runique makemigrations |
| Migrations (apply) | python manage.py migrate | runique migration up |
| Migrations (rollback) | python manage.py migrate app 0001 | runique migration down -n 1 |
| Migration status | python manage.py showmigrations | runique migration status |
| Create superuser | python manage.py createsuperuser | runique create-superuser |
| Tests | python manage.py test | runique test |
| Start | python manage.py runserver | cargo run |
Routing
| Feature | Django | Runique |
|---|---|---|
| URL declaration | urls.py with path() | url.rs with urlpatterns!{} macro |
| Dynamic routes | path('users/<int:id>/', view) | "/users/{id}" in urlpatterns! |
| Namespaces | app_name + include() | Router::new().nest("/prefix", ...) |
| Reverse URL | {% url "view_name" %} native | {% link "view_name" %} → custom Tera function |
| Typed path param | <int:id> in path(), converted to int | request.get_path_as::<i32>("id") |
| Raw path param | <str:id> | request.get_path("id") |
| Single query param | request.GET.get('key') | request.get_query("key") |
| Full query string | request.GET | request.query::<MyStruct>()? (deserializes to a Deserialize struct, 400 when the query doesn't fit) |
| HTTP headers | request.META['HTTP_X_FOO'] | request.headers.get("x-foo") |
Views / Handlers
| Feature | Django | Runique |
|---|---|---|
| Function view | def my_view(request) | async fn my_view(...) |
| Class view | class MyView(View) | — |
| Session access | request.session | request.session via context::template::Request |
| DB access | Model.objects.get(...) | Model::objects.get(...) or SeaORM query builders |
| Template render | render(request, "template.html", ctx) | request.render("template.html") |
| Redirect | redirect("url_name") | Redirect::to("/url") or reverse(&engine, "name") |
| Flash messages | messages.success(request, "...") | success!, error!, info!, warning! macros |
| Secondary connections | DATABASES['mongo'] | engine.extension::<mongodb::Client>() (multi-type TypeMap) |
Forms
| Feature | Django | Runique |
|---|---|---|
| Definition | class MyForm(ModelForm) | #[form(schema = blog, fields = [...])], tied to its entity by the schema, or manual RuniqueForm |
| Validation | form.is_valid() | ValidationForm::try_form(form, &request).await |
| Available fields | CharField, EmailField, FileField, etc. | TextField (+ ::email/::password/::url/::phone constructors → correct <input type> + validation), NumericField, BooleanField, CheckboxField, RadioField, ChoiceField, FileField, DateField, TimeField, DateTimeField, DurationField, ColorField, SlugField, UUIDField, IPAddressField, JSONField, HiddenField, MediaField, BinaryField |
| HTML rendering | {{ form.as_p }} | {% form.my_form %} (full) or {% form.my_form.field %} |
| CSRF included | automatic | automatic — injected before the first field |
| Save to DB | form.save() | validated.save(&db).await, on the validated form, for any form carrying its entity (FormEntity): #[form(schema = …)] or impl FormEntity |
| Data access | form.cleaned_data['key'] | validated.cleaned::<String>("key"), validated.cleaned::<i32>(...), etc. |
| Async validation | no | yes (DB access in clean()) |
| Cross-field validation | clean() | clean() async |
| File fields | FileField (no content validation by default) | FileField native multipart — type/size validation, magic bytes for images, SVG rejected, written to staging only after the CSRF token, then committed after validation (UUID name, no path traversal) |
| HTML sanitization | manual | sanitize_rich / sanitize_strict applied to richtext fields |
Templates
| Feature | Django | Runique |
|---|---|---|
| Engine | Django Template Language | Tera (Jinja2 / Django-like syntax) |
| Inheritance | {% extends %} / {% block %} | same |
| Static files | {% load static %} + {% static "file" %} | {% static "file" %} native |
| Media files | {{ MEDIA_URL }}file | {% media "file" %} native (variables supported) |
| URL reverse | {% url "name" %} | {% link "name" %} |
| CSRF | {% csrf_token %} | {% csrf %} |
| Messages | {% for m in messages %} | {% messages %} |
| I18n | {% trans "..." %} | — (t() and tf() on the Rust side) |
ORM / Database
| Feature | Django | Runique |
|---|---|---|
| ORM | Native Django ORM | SeaORM (Rust async) |
| Model definition | class User(models.Model) | model!{} macro (semantic types) |
| Auto migrations | yes (change detection) | runique makemigrations |
| Chainable QuerySet | User.objects.filter(...).order_by(...) | User::objects.filter(...).order_by(...) |
Strict .get() | raises MultipleObjectsReturned | .one() — returns Err if multiple rows |
| Random ordering | order_by('?') | .order_by_random() |
| Expression ordering | order_by(Lower("title").desc()) | .order_by_expr(expr, order) |
| Relations | ForeignKey, ManyToMany, OneToOne | Standard SeaORM relations |
| Transactions | with transaction.atomic() | request.db_atomic() or a SeaORM transaction |
| Multi-engine SQL | yes | PostgreSQL, MySQL/MariaDB, SQLite (3 SeaORM backends; MariaDB rides the MySQL driver with its own migration dialect) |
| Secondary connections | DATABASES multiple entries | .with_custom_db(client), one client per type (TypeMap) |
| Framework table extension | — | extend!{} — ALTER TABLE ADD COLUMN on eihwaz_* tables |
Authentication
| Feature | Django | Runique |
|---|---|---|
| Login / Logout | authenticate() + login() | authenticate_user() + login(), logout() |
| Is authenticated | request.user.is_authenticated | is_authenticated(&session).await |
| Current user | request.user | request.user |
| Route protection | @login_required | .login_required("/profile", "profile", view!(profile), "/login") |
| Sessions | native | tower-sessions (MemoryStore + DB fallback) |
| Brute force protection | django-axes (3rd party) | Native LoginGuard (auto lockout) |
| Password hashing | PBKDF2 by default, optional Argon2 | Argon2 by default |
| Password reset | signed token bound to the password hash (stateless) | DB-persisted hashed, single-use tokens (IDOR-hardened — keyed on user_id), configurable TTL via .token_ttl(...), customizable email template |
| Force logout | changing the password closes its sessions | changing the password too; invalidate_all(user_id) on RuniqueSessionStore |
Security
| Feature | Django | Runique |
|---|---|---|
| CSRF | native | native (constant-time validation) |
| CSP | native since Django 6.0 (ContentSecurityPolicyMiddleware), to enable | native (per-request nonce always active) |
| HSTS | SECURE_HSTS_SECONDS | native — emitted only over real HTTPS (enforce_https/ACME), max-age/includeSubDomains/preload configurable (preload opt-in) |
| SameSite cookies | configurable | Strict by default |
| HttpOnly cookies | by default | always true |
| Rate limiting | django-ratelimit (3rd party) | Native RateLimiter, declared per route — keyed on the client IP resolved through trusted proxies, otherwise the ConnectInfo peer; never an unvalidated header |
| Open Redirect | — | native — Location of 3xx responses and the Refresh header of any response, target read by the WHATWG URL parser (the one browsers use), refused when in doubt |
| CORS | django-cors-headers (3rd party) | native via .with_cors(...) |
| Permissions-Policy | — | native — secure preset by default (~20 features denied) |
| Trusted Proxies / XFF | SECURE_PROXY_SSL_HEADER (partial) | native — full XFF chain validation (right→left), RFC 1918 preset, edge-aware default: auto none() when Runique terminates TLS (ACME = no proxy); a /0 range (trusting everyone) is refused at startup |
| Secret key | manual | auto-generated on runique new — refuses to boot in prod if missing / default value / < 32 chars |
Admin Panel
| Feature | Django | Runique |
|---|---|---|
| Registration | admin.site.register(Model) | typed ModelAdmin + AdminSite.register(...) |
| Full CRUD | native | native |
| List pagination | native | .page_size(n) site-wide + .per_page(n) per model |
| Displayed columns | list_display | .list(|l| l.display([Title, Status])) — typed SeaORM columns |
| Search / filters | native | .search([...]) + .filter([...]) — direct columns only, combinable filters |
| Group actions | actions | .action(Col, value, "Label") — fixed value in code, UI sends the action index |
| Bulk edit | list_editable (several rows from the list) | batch edit on a selection |
| Bulk delete | native | native when the account may delete |
| M2M / list fields | filter_horizontal / ManyRelatedField | handled via model list fields (multichoice / checkbox) |
| Custom admin routes | get_urls() | admin "…" => … in urlpatterns! |
| Custom templates | yes | yes (Tera hierarchy + per-model .templates(...)) |
| Permissions | per resource | Ranks, admin rights per account (table and column) |
| Superuser accounts | a staff member allowed to edit users can make themselves a superuser (Django's docs warn about it) | is_superuser is never editable from the admin, and a superuser's account (email, password reset, deletion) can only be changed by a superuser |
| Change history | native (LogEntry, "History" link); field diffs with django-simple-history (3rd party) | native history (created/modified/deleted) with field diff, timeline view and filters (resource/action/user) |
| Extended accounts | AUTH_USER_MODEL (custom user model) | extend!{} + AdminSite::accounts_users(ModelAdmin) |
| Optimistic locking | — | native — detects concurrent edits (__original_updated_at) |
| Admin login brute-force protection | django-axes (3rd party) | native — LoginGuard + rate limiter on admin login |
| Account creation email | 3rd-party plugin | native — "choose my password" link sent to the account's address, activated on first set |
| Partial list refresh | — | native — list_partial HTMX without full page reload |
| Read-only / create-only fields | readonly_fields | .readonly([...]) / .create_only([...]) on the form builder |
| Fieldsets | fieldsets | — |
| Date hierarchy | date_hierarchy | — |
| Export | 3rd party (django-import-export) | — |
| Advanced save buttons | "Save and continue" / "Save and add another" | — |
| Feature | Django | Runique |
|---|---|---|
| Send mail | send_mail() native | Email::new().send() native |
| Email templates | native | Tera templates via .template("emails/my.html") |
| SMTP backend | configurable | configuration via .env |
| Dev backend (console) | EMAIL_BACKEND = 'console' | EMAIL_BACKEND=console in .env |
Observability / Logging
| Feature | Django | Runique |
|---|---|---|
| Logging config | LOGGING dict (Python logging) | RuniqueLog per-domain tree (forms, middleware, session, auth, admin, db, mailer, migration, templates, errors, builder) — each leaf an Option<LogLevel> |
| Structured tracing | 3rd party (structlog) | native (tracing); swallowed Result sites log their file:line via TraceResult::trace / trace_or |
| Outputs | handlers (console / file / …) | .output(...): colored stdout, rolling file (JSON or plain, non-blocking), custom LogSink (DB / HTTP / queue) |
| Runtime file override | — | RUNIQUE_LOG_FILE env var |
| App-owned subscriber | logging is app-owned | .external() — Runique installs nothing, still emits to the tracing facade |
Internationalization
| Feature | Django | Runique |
|---|---|---|
| Languages | unlimited | 9 default languages (compiled JSON) |
| Fallback | yes | yes (Lang::En) |
| Translation | _("...") | t("section.key") / tf("...", &[var]) in Rust, framework texts |
Performance & Deployment
| Aspect | Django | Runique |
|---|---|---|
| Runtime | CPython (interpreted) | Tokio async Rust (compiled) |
| Compilation | — | single binary |
| ACME / auto-TLS | certbot (external) | native via acme feature |
What Runique is still missing (compared to Django)
- Image resizing: no server-side auto resize/cropping.
- Generic public CRUD views: no equivalent to Django's
DetailView,ListView,CreateViewfor public views — considered viacrud!{}(roadmap, no commitment). The admin panel covers backoffice CRUD viaModelAdmin. - Model signals / hooks:
before_save,after_save,before_delete,after_deletecan't be declared inmodel!{}. The internalHooksDefstruct exists, with no link to the DSL; the behaviourmodel!{}generates only fillsauto_nowcolumns. On the roadmap, with no commitment. - Management commands: no equivalent to
manage.py custom_command— one-shot operations go throughsrc/bin/. - Built-in test client: no native HTTP test client.
runique testandrunique_testcover running tests and isolating the database; requests go throughreqwestortower::ServiceExt::oneshot. - Fixtures: no
loaddata/dumpdata— seeds are plain Rust functions. - Admin inline: no editing of related objects directly inside the parent form.
- Admin custom group actions:
.action(...)only supports fixed column updates — no arbitrary Rust logic on the selection (Django equivalent:actionswith any callable). - Admin FK filters:
.filter([...])supports direct columns only — no relation traversal (article__author__name). - Admin fieldsets: no field grouping by section in admin forms (
fieldsetsin Django). - Admin date hierarchy: no
date_hierarchyto filter the list by year/month/day. - Admin export: no native CSV/JSON export from the list view.
- Admin advanced save buttons: no "Save and continue editing" / "Save and add another".
- Full i18n:
t()andtf()are Rust functions for the framework's texts. No translation function in Tera templates, no pluralization, and a project can't add its own translations yet. - Background tasks: no task framework; Django has one since 6.0 (
@task, with an external worker). - Sitemap / RSS: no native generation.
.sitemap(url)only adds theSitemap:directive to the generated/robots.txt. - Third-party authentication:
Delegatedmode names providers (Google, Microsoft, Apple, LDAP, SAML, custom), but no flow is implemented. No JWT or API key auth. - Security observability: no CSP
report-uri/report-to, so violations aren't collected. Logins (successful, failed, lockouts) go through the logs (RuniqueLog) and aren't saved in the database;eihwaz_historytraces admin writes, rights and resets.
Known limits
Database and migrations
| Limit | Details |
|---|---|
One engine per build (sqlite, postgres, mysql)SQLite: one writer at a time, SQLITE_BUSY under heavy write loadSwitching engines needs a fresh database: data isn't carried over | Database |
A column type change isn't migrated (--force writes an empty migration, to complete by hand)Snapshots copied without their migrations make the tables look created SQL identifiers capped at 64 characters on MySQL / MariaDB migration up: a migration is atomic under Postgres only (SQLite, MySQL / MariaDB: a failing migration can stay half applied)A hard stop during makemigrations can leave half-written files (git to restore) | Migrations, Makemigrations |
search!: in (vec) with an empty vec, no inline conditional filter | Queries |
Forms and messages
| Limit | Details |
|---|---|
| A failed session write loses the flash message silently | Flash macros |
| A published file followed by a hard stop before the database commit can leave an orphan file | Fields |
Security, sessions, authentication
| Limit | Details |
|---|---|
RateLimiter and LoginGuard have no memory capRateLimiter and LoginGuard count per process: with several instances, the quota is multiplied | Rate limiting |
LoginGuard locks by username: someone can lock an account whose name they know | LoginGuard |
| The in-memory session store belongs to one process | Session store |
A custom store (with_session_store) can't be walked per user: a password reset doesn't close its sessions | Session |
Delegated mode: no external provider (OAuth, LDAP, SAML) is implemented | Passwords |
| ACME: one site per machine (port 80 exclusive) | Network |
Admin
| Limit | Details |
|---|---|
.links, .search_help, .empty_value accepted but with no effect on the display | ModelAdmin builder |
No global theme stylesheet (workaround through a block).with_htmx is global, no per-model list fragmentA missing page template or stylesheet is only reported when shown | Template override |
| No session management interface | — |
| Many-to-many: declared in the DSL through a pivot table, not managed in the admin | Roadmap |
Local development
| Limit | Details |
|---|---|
Cookies ignore the port: two projects on 127.0.0.1 share (and overwrite) the session cookie | Troubleshooting |
Solutions in the meantime
| Limit | Solution |
|---|---|
RateLimiter and LoginGuard have no memory cap, counters per process | Also limit at the reverse proxy (example below) |
LoginGuard blocks by username | Pair it with a limit per IP, at the reverse proxy or with RateLimiter |
| The memory session store belongs to one process | One instance, or a reverse proxy sending each visitor to the same one (ip_hash with nginx) |
| ACME: one site per machine | Hand TLS to a reverse proxy as soon as there are several sites |
| SQLite: one writer at a time | PostgreSQL for a sustained write load |
| A migration is atomic only on PostgreSQL | Back up the database before migration up on SQLite and MySQL / MariaDB |
| A column's type change isn't migrated | Complete by hand the migration written by --force |
Cookies shared by two projects on 127.0.0.1 | Open one on 127.0.0.1, the other on localhost |
Limit per IP with nginx, to adapt. The zone caps the memory, and a refused request gets a 503 by default:
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location /login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://127.0.0.1:3000;
}
What's coming
No dates, no commitment: a bug or a security flaw comes before planned work.
- Runique 4.0 release: finishing the 4.0 before its crates.io release, with tests on low-privilege and group accounts, and a full OWASP review.
- Security fixes (4.0.x): cap the number of keys held in memory by
RateLimiterandLoginGuard, and test open redirect protection against known URL validation bypass payloads. - 4.1, makemigrations: generate every schema operation with no manual step: dropping tables,
creating and dropping relation tables, honouring cascade, restrict and set_null, a real
ALTERon a type change, detecting default value changes, and columns both renamed and modified. Constraint names are read from the database, and destructive changes state what they affect. The many-to-many pivot table is generated from the model DSL; a hand-written table with extra columns remains possible. - 4.2, database:
get_or_create,update_or_create,exists(),last(), loading relations in one query, many-to-many access methods and its management in the admin.search!also accepts AND groups inside OR, and the negation of a condition or a group, still through typed queries. - Tracing: finish file output with rotation, add a request identifier, and leave no error silently ignored. Missing translation keys will be logged.
- Languages added by a project: let a project provide a language Runique does not ship, registered through the builder.
- Admin: date and foreign key filters, date hierarchy,
fieldsets, read-only fields, autocomplete, inline editing, group actions with Rust logic, CSV/JSON export, and "save and continue".
No commitment: these will move up as work finishes.
- Authentication: two-level brute force protection, then OAuth/OIDC, JWT, API keys, a login journal and CSP violation reports.
- ORM and DSL: model hooks, aggregates in
search!, declared indexes, and numeric bounds and steps. - Tools and storage: public CRUD views, a built-in test client, fixtures, custom commands, sitemap and RSS, image resizing, and a storage extension point for S3-compatible services.