Comparison

Runique next to Loco, Cot and Django, its known limits and what's coming.

Contents

Runique / Loco

TopicRunique 4.0Loco 1.3.1
InspirationDjangoRails
ServerAxumAxum
DatabaseSeaORM: SQLite, PostgreSQL, MySQL/MariaDB, one engine per buildSeaORM: SQLite, PostgreSQL
Migrationsmodel!{}, makemigrations, migration up/down/statuscargo loco generate model/migration, db migrate/down/status
ViewsTeraTera, replaceable engine
Forms#[form]: validation and HTML renderingExtraction and validation with validator
AdministrationBuilt in, rights per accountUp to the project
AuthenticationLogin, sessions, password resetJWT, sessions, OAuth, reset in generated applications
Web securityCSRF always on, headers and CSP by default; hosts and rate limit to declaresecure_headers to enable (presets including CSP); rate limit through a tower layer
Background jobsUp to the projectWorkers, scheduler
Cache and storageExternal connection registered by the project; local mediaMemory and Redis cache; S3, GCP, Azure
GeneratorsProject and migrationsModel, controller, scaffold, worker, mailer, task, deployment
LicenseMITApache-2.0

Checked on November 5, 2026 in the documentation and the Loco repository.

Runique / Cot

TopicRunique 4.0Cot 0.7
InspirationDjangoDjango
ServerAxumhyper and tower
DatabaseSeaORM: one engine per buildOwn ORM (sea-query, sqlx): SQLite, PostgreSQL, MySQL
Migrationsmakemigrations, migration up/down/statuscot migration make, applied at startup, migration rollback
ViewsTeraAskama
Forms#[form]#[derive(Form)], file fields
AdministrationModelAdmin, rights per accountBuilt in (AdminModel), access by login
AuthenticationLogin, sessions, password resetDatabaseUser, sessions
SessionsMemory with database fallback, custom storeMemory, database, file, Redis
Web securityCSRF always on, headers and CSP by default; hosts and rate limit to declareConfigurable SameSite; the rest up to the project
APIUp to the projectOptional OpenAPI and Swagger
Email and cacheBuilt-in email; cache up to the projectEmail; memory and Redis cache
Testsrunique test, transactional isolationcot::test, end-to-end (e2e_test)
LicenseMITMIT 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.

CommandDjangoRunique
Create projectdjango-admin startproject namerunique new name
Create apppython manage.py startapp name—
Migrations (generate)python manage.py makemigrationsrunique makemigrations
Migrations (apply)python manage.py migraterunique migration up
Migrations (rollback)python manage.py migrate app 0001runique migration down -n 1
Migration statuspython manage.py showmigrationsrunique migration status
Create superuserpython manage.py createsuperuserrunique create-superuser
Testspython manage.py testrunique test
Startpython manage.py runservercargo run

Routing

FeatureDjangoRunique
URL declarationurls.py with path()url.rs with urlpatterns!{} macro
Dynamic routespath('users/<int:id>/', view)"/users/{id}" in urlpatterns!
Namespacesapp_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 intrequest.get_path_as::<i32>("id")
Raw path param<str:id>request.get_path("id")
Single query paramrequest.GET.get('key')request.get_query("key")
Full query stringrequest.GETrequest.query::<MyStruct>()? (deserializes to a Deserialize struct, 400 when the query doesn't fit)
HTTP headersrequest.META['HTTP_X_FOO']request.headers.get("x-foo")

Views / Handlers

FeatureDjangoRunique
Function viewdef my_view(request)async fn my_view(...)
Class viewclass MyView(View)—
Session accessrequest.sessionrequest.session via context::template::Request
DB accessModel.objects.get(...)Model::objects.get(...) or SeaORM query builders
Template renderrender(request, "template.html", ctx)request.render("template.html")
Redirectredirect("url_name")Redirect::to("/url") or reverse(&engine, "name")
Flash messagesmessages.success(request, "...")success!, error!, info!, warning! macros
Secondary connectionsDATABASES['mongo']engine.extension::<mongodb::Client>() (multi-type TypeMap)

Forms

FeatureDjangoRunique
Definitionclass MyForm(ModelForm)#[form(schema = blog, fields = [...])], tied to its entity by the schema, or manual RuniqueForm
Validationform.is_valid()ValidationForm::try_form(form, &request).await
Available fieldsCharField, 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 includedautomaticautomatic — injected before the first field
Save to DBform.save()validated.save(&db).await, on the validated form, for any form carrying its entity (FormEntity): #[form(schema = …)] or impl FormEntity
Data accessform.cleaned_data['key']validated.cleaned::<String>("key"), validated.cleaned::<i32>(...), etc.
Async validationnoyes (DB access in clean())
Cross-field validationclean()clean() async
File fieldsFileField (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 sanitizationmanualsanitize_rich / sanitize_strict applied to richtext fields

Templates

FeatureDjangoRunique
EngineDjango Template LanguageTera (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

FeatureDjangoRunique
ORMNative Django ORMSeaORM (Rust async)
Model definitionclass User(models.Model)model!{} macro (semantic types)
Auto migrationsyes (change detection)runique makemigrations
Chainable QuerySetUser.objects.filter(...).order_by(...)User::objects.filter(...).order_by(...)
Strict .get()raises MultipleObjectsReturned.one() — returns Err if multiple rows
Random orderingorder_by('?').order_by_random()
Expression orderingorder_by(Lower("title").desc()).order_by_expr(expr, order)
RelationsForeignKey, ManyToMany, OneToOneStandard SeaORM relations
Transactionswith transaction.atomic()request.db_atomic() or a SeaORM transaction
Multi-engine SQLyesPostgreSQL, MySQL/MariaDB, SQLite (3 SeaORM backends; MariaDB rides the MySQL driver with its own migration dialect)
Secondary connectionsDATABASES multiple entries.with_custom_db(client), one client per type (TypeMap)
Framework table extension—extend!{} — ALTER TABLE ADD COLUMN on eihwaz_* tables

Authentication

FeatureDjangoRunique
Login / Logoutauthenticate() + login()authenticate_user() + login(), logout()
Is authenticatedrequest.user.is_authenticatedis_authenticated(&session).await
Current userrequest.userrequest.user
Route protection@login_required.login_required("/profile", "profile", view!(profile), "/login")
Sessionsnativetower-sessions (MemoryStore + DB fallback)
Brute force protectiondjango-axes (3rd party)Native LoginGuard (auto lockout)
Password hashingPBKDF2 by default, optional Argon2Argon2 by default
Password resetsigned 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 logoutchanging the password closes its sessionschanging the password too; invalidate_all(user_id) on RuniqueSessionStore

Security

FeatureDjangoRunique
CSRFnativenative (constant-time validation)
CSPnative since Django 6.0 (ContentSecurityPolicyMiddleware), to enablenative (per-request nonce always active)
HSTSSECURE_HSTS_SECONDSnative — emitted only over real HTTPS (enforce_https/ACME), max-age/includeSubDomains/preload configurable (preload opt-in)
SameSite cookiesconfigurableStrict by default
HttpOnly cookiesby defaultalways true
Rate limitingdjango-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
CORSdjango-cors-headers (3rd party)native via .with_cors(...)
Permissions-Policy—native — secure preset by default (~20 features denied)
Trusted Proxies / XFFSECURE_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 keymanualauto-generated on runique new — refuses to boot in prod if missing / default value / < 32 chars

Admin Panel

FeatureDjangoRunique
Registrationadmin.site.register(Model)typed ModelAdmin + AdminSite.register(...)
Full CRUDnativenative
List paginationnative.page_size(n) site-wide + .per_page(n) per model
Displayed columnslist_display.list(|l| l.display([Title, Status])) — typed SeaORM columns
Search / filtersnative.search([...]) + .filter([...]) — direct columns only, combinable filters
Group actionsactions.action(Col, value, "Label") — fixed value in code, UI sends the action index
Bulk editlist_editable (several rows from the list)batch edit on a selection
Bulk deletenativenative when the account may delete
M2M / list fieldsfilter_horizontal / ManyRelatedFieldhandled via model list fields (multichoice / checkbox)
Custom admin routesget_urls()admin "…" => … in urlpatterns!
Custom templatesyesyes (Tera hierarchy + per-model .templates(...))
Permissionsper resourceRanks, admin rights per account (table and column)
Superuser accountsa 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 historynative (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 accountsAUTH_USER_MODEL (custom user model)extend!{} + AdminSite::accounts_users(ModelAdmin)
Optimistic locking—native — detects concurrent edits (__original_updated_at)
Admin login brute-force protectiondjango-axes (3rd party)native — LoginGuard + rate limiter on admin login
Account creation email3rd-party pluginnative — "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 fieldsreadonly_fields.readonly([...]) / .create_only([...]) on the form builder
Fieldsetsfieldsets—
Date hierarchydate_hierarchy—
Export3rd party (django-import-export)—
Advanced save buttons"Save and continue" / "Save and add another"—

Email

FeatureDjangoRunique
Send mailsend_mail() nativeEmail::new().send() native
Email templatesnativeTera templates via .template("emails/my.html")
SMTP backendconfigurableconfiguration via .env
Dev backend (console)EMAIL_BACKEND = 'console'EMAIL_BACKEND=console in .env

Observability / Logging

FeatureDjangoRunique
Logging configLOGGING dict (Python logging)RuniqueLog per-domain tree (forms, middleware, session, auth, admin, db, mailer, migration, templates, errors, builder) — each leaf an Option<LogLevel>
Structured tracing3rd party (structlog)native (tracing); swallowed Result sites log their file:line via TraceResult::trace / trace_or
Outputshandlers (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 subscriberlogging is app-owned.external() — Runique installs nothing, still emits to the tracing facade

Internationalization

FeatureDjangoRunique
Languagesunlimited9 default languages (compiled JSON)
Fallbackyesyes (Lang::En)
Translation_("...")t("section.key") / tf("...", &[var]) in Rust, framework texts

Performance & Deployment

AspectDjangoRunique
RuntimeCPython (interpreted)Tokio async Rust (compiled)
Compilation—single binary
ACME / auto-TLScertbot (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, CreateView for public views — considered via crud!{} (roadmap, no commitment). The admin panel covers backoffice CRUD via ModelAdmin.
  • Model signals / hooks: before_save, after_save, before_delete, after_delete can't be declared in model!{}. The internal HooksDef struct exists, with no link to the DSL; the behaviour model!{} generates only fills auto_now columns. On the roadmap, with no commitment.
  • Management commands: no equivalent to manage.py custom_command — one-shot operations go through src/bin/.
  • Built-in test client: no native HTTP test client. runique test and runique_test cover running tests and isolating the database; requests go through reqwest or tower::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: actions with 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 (fieldsets in Django).
  • Admin date hierarchy: no date_hierarchy to 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() and tf() 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 the Sitemap: directive to the generated /robots.txt.
  • Third-party authentication: Delegated mode 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_history traces admin writes, rights and resets.

Known limits

Database and migrations

LimitDetails
One engine per build (sqlite, postgres, mysql)
SQLite: one writer at a time, SQLITE_BUSY under heavy write load
Switching 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 filterQueries

Forms and messages

LimitDetails
A failed session write loses the flash message silentlyFlash macros
A published file followed by a hard stop before the database commit can leave an orphan fileFields

Security, sessions, authentication

LimitDetails
RateLimiter and LoginGuard have no memory cap
RateLimiter 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 knowLoginGuard
The in-memory session store belongs to one processSession store
A custom store (with_session_store) can't be walked per user: a password reset doesn't close its sessionsSession
Delegated mode: no external provider (OAuth, LDAP, SAML) is implementedPasswords
ACME: one site per machine (port 80 exclusive)Network

Admin

LimitDetails
.links, .search_help, .empty_value accepted but with no effect on the displayModelAdmin builder
No global theme stylesheet (workaround through a block)
.with_htmx is global, no per-model list fragment
A 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 adminRoadmap

Local development

LimitDetails
Cookies ignore the port: two projects on 127.0.0.1 share (and overwrite) the session cookieTroubleshooting

Solutions in the meantime

LimitSolution
RateLimiter and LoginGuard have no memory cap, counters per processAlso limit at the reverse proxy (example below)
LoginGuard blocks by usernamePair it with a limit per IP, at the reverse proxy or with RateLimiter
The memory session store belongs to one processOne instance, or a reverse proxy sending each visitor to the same one (ip_hash with nginx)
ACME: one site per machineHand TLS to a reverse proxy as soon as there are several sites
SQLite: one writer at a timePostgreSQL for a sustained write load
A migration is atomic only on PostgreSQLBack up the database before migration up on SQLite and MySQL / MariaDB
A column's type change isn't migratedComplete by hand the migration written by --force
Cookies shared by two projects on 127.0.0.1Open 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 RateLimiter and LoginGuard, 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 ALTER on 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.

See the known issues by version