← Comparison

Runique vs Django

Contents

CLI

Real commands of the runique binary (see runique/src/bin/runique.rs): new, create-superuser, test, makemigrations, migration up | down | status. There is no runique start — admin is plain Rust, cargo run is enough.

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 / MediaField native multipart — type/size validation, content signature for images, audio and video, active extensions (.svg, .html, .js…) 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_asc(Col)
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(&tera, "emails/my.html", ctx.into())
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.

What's coming in Runique