Comparatif

Runique face à Loco, Cot et Django, ses limites connues et ce qui arrive.

Sommaire

Runique / Loco

CLI

CommandeLocoRunique
Installercargo install lococargo install runique --features sqlite --locked
Créer un projetloco new (assistant : type d'application, base, worker)runique new nom
Migrations (générer)cargo loco generate model / generate migrationrunique makemigrations
Migrations (appliquer)cargo loco db migraterunique migration up
Migrations (annuler)cargo loco db downrunique migration down -n 1
Statut migrationscargo loco db statusrunique migration status
Régénérer les entitéscargo loco db entities— (model!{} les génère à la compilation)
Générateurs de codemodèle, contrôleur, scaffold, worker, mailer, tâche, planificateur, déploiement—
Lister les routescargo loco routes—
Lister les middlewarescargo loco middleware—
Diagnosticcargo loco doctor—
Créer un superutilisateur—runique create-superuser
Démarrercargo loco startcargo run
Rechargement du codecargo loco watch (avec cargo-watch)—

Routing

FonctionnalitéLocoRunique
Déclaration des routesRoutes::new().prefix("notes").add("/", get(list))url.rs avec macro urlpatterns!{}
Routes dynamiques.add("/{id}", get(get_one))"/users/{id}" dans urlpatterns!
Reverse URL—{% link "nom_vue" %}
Paramètre de cheminextracteur Axum PathPath Axum ou request.get_path_as::<i32>("id")
Query stringQueryValidate<T> ou extracteur Axumrequest.query::<MyStruct>()?
Préfixe d'applicationAppRoutesRouter::new().nest("/prefix", ...)

Vues / Handlers

FonctionnalitéLocoRunique
Handlerasync fn hello() -> Result<Response>async fn ma_vue(mut request: Request) -> AppResult<Response>
Accès DBState(ctx): State<AppContext>, puis ctx.dbrequest.engine.db
Renduformat::view(&v, "home/hello.html", data!({...}))request.render("template.html")
Réponse JSONformat::json(res)Json d'Axum
Redirectionformat::redirect("/url")Redirect::to("/url")
Messages flash—macros success!, error!, info!, warning!
Connexions secondairesMultiDbInitializer, connexions nommées.with_custom_db::<T>()

Formulaires

FonctionnalitéLocoRunique
Définitionstruct #[derive(Deserialize, Validate)] (crate validator)#[form(schema = blog, fields = [...])] ou RuniqueForm manuel
Lecture et validationextracteurs FormValidate<T>, JsonValidate<T>, QueryValidate<T>ValidationForm::try_form(form, &request).await
Rendu HTML—{% form.nom_form %} ou {% form.nom_form.champ %}
CSRF intégré—automatique, injecté avant le premier champ
Sauvegardecode du modèle (SeaORM)validated.save(&db).await, le formulaire porte son entité (FormEntity)
Validation croiséeValidatorTraitclean() async, avec accès à la base
FichiersMultipart d'Axum, stockage configurableFileField et MediaField : type, taille, magic bytes pour les images
Stockagedisque, mémoire, S3, GCP, Azuredossier MEDIA_ROOT

Templates

FonctionnalitéLocoRunique
MoteurTera, moteur remplaçable (ViewRenderer)Tera
Emplacementassets/views/templates/
Fichiers statiquesmiddleware static (assets/static){% static "file" %}, avec SRI en production
Fichiers media—{% media "file" %}
URL reverse—{% link "nom" %}
CSRF—{% csrf %}
Messages—{% messages %}
Binaire autonomefeature embedded_assets—

ORM / Base de données

FonctionnalitéLocoRunique
ORMSeaORMSeaORM
MoteursSQLite, PostgreSQL (features par défaut)SQLite, PostgreSQL, MySQL/MariaDB, un moteur par compilation
Définition modèleentités générées depuis les migrationsmacro model!{}
MigrationsDSL loco_rs::schema, écrites ou généréesrunique makemigrations compare au dernier instantané
Requêtesquery::condition() et SeaORMsearch! et SeaORM
Paginationquery::paginate.paginate(...) de SeaORM
TransactionsSeaORMrequest.db_atomic() et SeaORM
Multi-tenantfeature multi-tenancy—
Extension table framework—extend!{} sur les tables eihwaz_*

Authentification

FonctionnalitéLocoRunique
Login / Logoutroutes /api/auth/* généréesauthenticate_user() + login(), logout()
JWTauth::JWT, auth::JWTWithUser<T>—
Clé d'APIfind_by_api_key—
Sessionscookie signé ou chiffré, stores base et Redis (feature auth_session)tower-sessions (mémoire + repli en base)
OAuth2 / OIDCfeature auth_oauth—
Protection brute force—LoginGuard
Hashage mot de passeArgon2id (hash::hash_password)Argon2 par défaut
Reset passwordroutes forgot et reset généréesjetons hashés en base, à usage unique, avec expiration
Droits—rangs et droits des admins par compte

Sécurité

FonctionnalitéLocoRunique
CSRF—toujours actif
CSP et en-têtessecure_headers à activer, présets github et owaspactifs par défaut, nonce par requête
Rate limitingcouche tower choisie par le projetRateLimiter, déclaré par route
CORSmiddleware cors.with_cors(...)
Hôtes autorisés—.with_allowed_hosts(...)
Open Redirect—middleware toujours actif
IP réelle derrière un proxymiddleware remote_ip.with_trusted_proxies(...)

Panel Admin

FonctionnalitéLocoRunique
Activation—builder typé ModelAdmin + AdminSite.register(...)
CRUD, recherche, filtres—natif
Permissions—rangs, droits des admins par compte (table et colonne)
Historique—natif

Tâches de fond

FonctionnalitéLocoRunique
Workerstrait Worker, files Redis, PostgreSQL, SQLite ou threads—
Planificateurcargo loco scheduler, expressions cron—
Tâches ponctuellescargo loco task—
Cachemémoire, Redis, base de données—

Email

FonctionnalitéLocoRunique
Envoimailers envoyés par un workerEmail::new().send()
Templatessubject.t, html.t, text.t (Tera)templates Tera via .template("emails/mon.html")
Backend SMTPfichiers config/*.yaml.env
Backend dev (console)—EMAIL_BACKEND=console

Observabilité / Logging

FonctionnalitéLocoRunique
Config loggingclés logger en YAMLarbre RuniqueLog par domaine
Basetracingtracing
Formatscompact, pretty, jsonstdout, fichier JSON ou texte, LogSink
PrioritéRUST_LOGRUST_LOG

Tests

FonctionnalitéLocoRunique
Commandecargo testrunique test
Requêtes HTTPrequest::<App, _, _>(), TestServerreqwest ou tower::ServiceExt::oneshot
Isolation baserequest_with_create_db, #[serial]runique_test, transaction par test

Déploiement

AspectLocoRunique
Configurationconfig/*.yaml, LOCO_ENV.env, RUNIQUE_ENV
Livraisonbinaire <package>-clibinaire
GénérateursDocker, nginx, AWS Lambda—
TLS automatique—feature acme
LicenceApache-2.0MIT

Runique / Cot

CLI

CommandeCotRunique
Installercargo install --locked cot-clicargo install runique --features sqlite --locked
Créer un projetcot new nomrunique new nom
Migrations (générer)cot migration makerunique makemigrations
Migration videcot migration new <nom>—
Migrations (appliquer)automatique au démarragerunique migration up
Migrations (annuler)cargo run migration rollback <nom>runique migration down -n 1
Statut migrationscot migration listrunique migration status
Créer un superutilisateurcode de App::init dans l'exemple du guiderunique create-superuser
Collecter les statiquescargo run -- collect-static public/—
Démarrercargo runcargo run
Rechargement du codebacon (bacon.toml généré)—

Routing

FonctionnalitéCotRunique
Déclaration des routesRouter::with_urls([Route::with_handler_and_name("/", index, "index")])url.rs avec macro urlpatterns!{}
Routes dynamiques"/hello/{name}""/users/{id}" dans urlpatterns!
Reverse URLcot::reverse!(urls, "nom"){% link "nom_vue" %}
Paramètre de cheminextracteur Path(name): Path<String>Path Axum ou request.get_path_as::<i32>("id")

Vues / Handlers

FonctionnalitéCotRunique
Handlerasync fn index() -> cot::Result<Html>async fn ma_vue(mut request: Request) -> AppResult<Response>
Accès DBextracteur db: Databaserequest.engine.db
Rendu templateIndexTemplate { .. }.render()?request.render("template.html")
Redirectionreverse_redirect!(request, "nom")Redirect::to("/url")
Messages flash—macros success!, error!, info!, warning!
Pages d'erreurerror_handler(), page de debug avec debug = truetemplates 404.html, 500.html, page de debug avec DEBUG=true
APIfeature openapi, swagger-ui—

Formulaires

FonctionnalitéCotRunique
Définition#[derive(Form)]#[form(schema = blog, fields = [...])] ou RuniqueForm manuel
Lecture et validationContactForm::from_request(&mut request).await?ValidationForm::try_form(form, &request).await
Validation intégrée#[form(opts(max_length = 100))]contraintes du modèle ou du champ
Validation personnaliséecontext.add_error(FormErrorTarget::Field("title"), ...)clean() async, avec accès à la base
Rendu HTML{{ form }} ou form.fields(){% form.nom_form %} ou {% form.nom_form.champ %}
CSRF intégré—automatique, injecté avant le premier champ
Fichiersform/fields/files.rsFileField et MediaField : type, taille, magic bytes pour les images

Templates

FonctionnalitéCotRunique
MoteurAskama, templates liés à une structTera
Héritage{% extends %} / {% block %}idem
Inclusion{% include %}idem
Fichiers statiquesstatic_files!(...), extracteur StaticFiles{% static "file" %}, avec SRI en production
Version des statiquesrewrite = "query_param"?v= automatique
URL reversecot::reverse!(urls, "nom"){% link "nom" %}
Internationalisation—t() et tf() en Rust, 9 langues pour les textes du framework

ORM / Base de données

FonctionnalitéCotRunique
ORMORM propre (sea-query, sqlx)SeaORM
MoteursSQLite, PostgreSQL, MySQL, activés ensemble par défautSQLite, PostgreSQL, MySQL/MariaDB, un moteur par compilation
Définition modèle#[model] sur une structmacro model!{}
Clé étrangèreForeignKey<User>belongs_to, avec action de suppression obligatoire
Requêtesquery!(Customer, $id == 5), Customer::objects()search!, Model::objects
Insertion en lotbulk_insertSeaORM
Transactionsdb.begin(), imbrication par savepointsrequest.db_atomic() et SeaORM
Migration personnalisée#[migration_op], forwards et backwardsmigration SeaORM écrite à la main

Authentification

FonctionnalitéCotRunique
Login / Logoutextracteur Auth : auth.login(...), auth.logout()authenticate_user() + login(), logout()
UtilisateursDatabaseUser, DatabaseUserApptable eihwaz_users, extensible par extend!{}
Sessionsstores mémoire, base, fichier, Redistower-sessions (mémoire + repli en base), store personnalisable
Cookie de sessionSecure, HttpOnly, SameSite=Strict par défaut, configurablesHttpOnly, SameSite=Strict, Secure en production
Hashage mot de passeArgon2Argon2 par défaut
Protection brute force—LoginGuard
Reset password—jetons hashés en base, à usage unique, avec expiration

Sécurité

FonctionnalitéCotRunique
CSRF—toujours actif
CSP et en-têtes de sécurité—actifs par défaut, nonce par requête
Rate limiting—RateLimiter, déclaré par route
CORS—.with_cors(...)
Hôtes autorisés—.with_allowed_hosts(...)
Open Redirect—middleware toujours actif

Panel Admin

FonctionnalitéCotRunique
Activation#[derive(AdminModel)] + DefaultAdminModelManagerbuilder typé ModelAdmin + AdminSite.register(...)
CRUD completnatifnatif
Paginationnative.page_size(n), .per_page(n)
Recherche / filtres—.search([...]), .filter([...])
Actions de groupe—.action(Col, value, "Libellé")
Accèsutilisateur connectérangs, droits des admins par compte (table et colonne)
Historique—natif

Email et cache

FonctionnalitéCotRunique
Envoi emailEmailMessage::builder(), feature emailEmail::new().send()
Backend dev (console)type = "console"EMAIL_BACKEND=console
Configuration[email] en TOML.env
Cachemémoire, Redis, fichier (Cache)—

Observabilité / Logging

FonctionnalitéCotRunique
Basetracingtracing
Configuration—arbre RuniqueLog par domaine, stdout, fichier, LogSink

Tests

FonctionnalitéCotRunique
Commandecargo testrunique test
UnitairesTestRequestBuildertower::ServiceExt::oneshot
IntégrationClient::new(MyProject)reqwest
Base de testTestDatabase::new_sqlite(), new_postgres, new_mysqlrunique_test, transaction par test
Bout en bout#[cot::e2e_test]—

Déploiement

AspectCotRunique
Configurationconfig/dev.toml, config/prod.toml.env, RUNIQUE_ENV
TLS automatique—feature acme
LicenceMIT ou Apache-2.0MIT

Runique / Django

CLI

Commandes réelles du binaire runique (voir runique/src/bin/runique.rs) : new, create-superuser, test, makemigrations, migration up. Pas de runique start — l'admin est du Rust plain, cargo run suffit. Annuler / lister les migrations passe par SeaORM, pas par un sous-commande Runique.

CommandeDjangoRunique
Créer un projetdjango-admin startproject nomrunique new nom
Créer une apppython manage.py startapp nom—
Migrations (générer)python manage.py makemigrationsrunique makemigrations
Migrations (appliquer)python manage.py migraterunique migration up
Migrations (annuler)python manage.py migrate app 0001runique migration down -n 1
Statut migrationspython manage.py showmigrationsrunique migration status
Créer superuserpython manage.py createsuperuserrunique create-superuser
Testspython manage.py testrunique test
Démarrerpython manage.py runservercargo run

Routing

FonctionnalitéDjangoRunique
Déclaration des routesurls.py avec path()url.rs avec macro urlpatterns!{}
Routes dynamiquespath('users/<int:id>/', view)"/users/{id}" dans urlpatterns!
Namespacesapp_name + include()Router::new().nest("/prefix", ...)
Reverse URL{% url "nom_vue" %} natif{% link "nom_vue" %} → fonction Tera custom
Paramètre de chemin typé<int:id> dans path(), converti en intrequest.get_path_as::<i32>("id")
Paramètre de chemin brut<str:id>request.get_path("id")
Query param uniquerequest.GET.get('key')request.get_query("key")
Query string complèterequest.GETrequest.query::<MyStruct>()? (désérialise vers un struct Deserialize, 400 si la query ne correspond pas)
Headers HTTPrequest.META['HTTP_X_FOO']request.headers.get("x-foo")

Vues / Handlers

FonctionnalitéDjangoRunique
Vue fonctiondef ma_vue(request)async fn ma_vue(...)
Vue classeclass MaVue(View)—
Accès sessionrequest.sessionrequest.session via context::template::Request
Accès DBModel.objects.get(...)Model::objects.get(...) ou builders SeaORM
Rendu templaterender(request, "template.html", ctx)request.render("template.html")
Redirectredirect("nom_url")Redirect::to("/url") ou reverse(&engine, "nom")
Messages flashmessages.success(request, "...")macros success!, error!, info!, warning!
Connexions secondairesDATABASES['mongo']engine.extension::<mongodb::Client>() (TypeMap multi-type)

Formulaires

FonctionnalitéDjangoRunique
Définitionclass MonForm(ModelForm)#[form(schema = blog, fields = [...])], lié à son entité par le schéma, ou RuniqueForm manuel
Validationform.is_valid()ValidationForm::try_form(form, &request).await
Champs disponiblesCharField, EmailField, FileField, etc.TextField (+ constructeurs ::email/::password/::url/::phone → bon <input type> + validation), NumericField, BooleanField, CheckboxField, RadioField, ChoiceField, FileField, DateField, TimeField, DateTimeField, DurationField, ColorField, SlugField, UUIDField, IPAddressField, JSONField, HiddenField, MediaField, BinaryField
Rendu HTML{{ form.as_p }}{% form.nom_form %} (entier) ou {% form.nom_form.champ %}
CSRF intégréautomatiqueautomatique — injecté avant le premier champ
Sauvegardeform.save()validated.save(&db).await, sur le formulaire validé, pour tout formulaire qui porte son entité (FormEntity) : #[form(schema = …)] ou impl FormEntity
Accès aux donnéesform.cleaned_data['clé']validated.cleaned::<String>("clé"), validated.cleaned::<i32>(...), etc.
Validation asyncnonoui (accès DB dans clean())
Validation croiséeclean()clean() async
FichiersFileField (pas de validation de contenu par défaut)FileField multipart natif — validation type/taille, magic bytes pour les images, SVG rejeté, écriture en staging seulement après le jeton CSRF, puis commit après validation (nom UUID, pas de path traversal)
Sanitisation HTMLnon (à la main)sanitize_rich / sanitize_strict appliquées aux champs richtext

Templates

FonctionnalitéDjangoRunique
MoteurDjango Template LanguageTera (syntaxe Jinja2 / Django-like)
Héritage{% extends %} / {% block %}idem
Fichiers statiques{% load static %} + {% static "file" %}{% static "file" %} natif
Fichiers media{{ MEDIA_URL }}file{% media "file" %} natif (variables supportées)
URL reverse{% url "nom" %}{% link "nom" %}
CSRF{% csrf_token %}{% csrf %}
Messages{% for m in messages %}{% messages %}
Internationalisation{% trans "..." %}— (t() et tf() côté Rust)

ORM / Base de données

FonctionnalitéDjangoRunique
ORMDjango ORM natifSeaORM (Rust async)
Définition modèleclass User(models.Model)macro model!{} (types sémantiques)
Migrations autooui (détection changements)runique makemigrations
QuerySet chaînableUser.objects.filter(...).order_by(...)User::objects.filter(...).order_by(...)
.get() strictlève MultipleObjectsReturned.one() — retourne Err si plusieurs lignes
Tri aléatoireorder_by('?').order_by_random()
Tri par expressionorder_by(Lower("titre").desc()).order_by_expr(expr, order)
RelationsForeignKey, ManyToMany, OneToOneRelations SeaORM standard
Transactionswith transaction.atomic()request.db_atomic() ou transaction SeaORM
Multi-moteur SQLouiPostgreSQL, MySQL/MariaDB, SQLite (3 backends SeaORM ; MariaDB passe par le driver MySQL avec son propre dialecte de migration)
Connexions secondairesDATABASES multi-entrées.with_custom_db(client), un client par type (TypeMap)
Extension table framework—extend!{} — ALTER TABLE ADD COLUMN sur tables eihwaz_*

Authentification

FonctionnalitéDjangoRunique
Login / Logoutauthenticate() + login()authenticate_user() + login(), logout()
Vérif authentificationrequest.user.is_authenticatedis_authenticated(&session).await
Utilisateur courantrequest.userrequest.user
Protection route@login_required.login_required("/profil", "profil", view!(profil), "/login")
Sessionsnatiftower-sessions (MemoryStore + DB fallback)
Protection brute forcedjango-axes (tiers)LoginGuard natif (lockout auto)
Hashage mot de passePBKDF2 par défaut, Argon2 en optionArgon2 par défaut
Reset passwordtoken signé lié au hash du mot de passe (stateless)tokens persistés en DB, hashés, single-use (durcis IDOR — clé user_id), TTL configurable via .token_ttl(...), template email personnalisable
Déconnexion forcéechanger le mot de passe ferme ses sessionschanger le mot de passe aussi ; invalidate_all(user_id) sur RuniqueSessionStore

Sécurité

FonctionnalitéDjangoRunique
CSRFnatifnatif (comparaison constant-time)
CSPnatif depuis Django 6.0 (ContentSecurityPolicyMiddleware), à activernatif (nonce par requête toujours actif)
HSTSSECURE_HSTS_SECONDSnatif — émis uniquement sur HTTPS réel (enforce_https/ACME), max-age/includeSubDomains/preload configurables (preload opt-in)
SameSite cookiesconfigurableStrict par défaut
HttpOnly cookiespar défauttoujours true
Rate limitingdjango-ratelimit (tiers)RateLimiter natif, déclaré par route — clé = IP cliente résolue par les proxys de confiance, sinon le pair ConnectInfo ; jamais un en-tête non validé
Open Redirect—natif — Location des réponses 3xx et en-tête Refresh de toute réponse, cible lue par l'analyseur d'URL WHATWG (le même que les navigateurs), refus en cas de doute
CORSdjango-cors-headers (tiers)natif via .with_cors(...)
Permissions-Policy—natif — preset sécurisé par défaut (~20 features refusées)
Trusted Proxies / XFFSECURE_PROXY_SSL_HEADER (partiel)natif — validation chaîne XFF complète (droite→gauche), preset RFC 1918, défaut edge-aware : none() auto quand Runique termine le TLS (ACME = pas de proxy) ; une plage /0 (faire confiance à tout le monde) est refusée au démarrage
Secret keymanuelgénérée auto à runique new — refus de boot en prod si absente / valeur par défaut / < 32 caractères

Panel Admin

FonctionnalitéDjangoRunique
Activationadmin.site.register(Model)builder typé ModelAdmin + AdminSite.register(...)
CRUD completnatifnatif
Pagination listenatif.page_size(n) au site + .per_page(n) par modèle
Colonnes affichéeslist_display.list(|l| l.display([Title, Status])) — colonnes SeaORM typées
Recherche / filtresnatif.search([...]) + .filter([...]) — colonnes directes uniquement, filtres cumulables
Actions de groupeactions.action(Col, value, "Libellé") — valeur fixée dans le code, l'UI envoie l'index
Édition en masselist_editable (plusieurs lignes depuis la liste)édition en lot sur une sélection
Suppression en massenatifnatif si le compte peut supprimer
Relations M2M / listesfilter_horizontal / ManyRelatedFieldchamps liste du modèle (multichoice / checkbox)
Routes admin customget_urls()admin "…" => … dans urlpatterns!
Templates customouioui (hiérarchie Tera + .templates(...) par modèle)
Permissionspar ressourceRangs, droits des admins par compte (table et colonne)
Comptes superutilisateurun staff ayant le droit de modifier les utilisateurs peut se rendre superutilisateur (la doc de Django le signale)is_superuser jamais modifiable depuis l'admin, et le compte d'un superutilisateur (email, réinitialisation, suppression) n'est modifiable que par un superutilisateur
Historique modificationsnatif (LogEntry, lien « History ») ; diffs de champs avec django-simple-history (tiers)historique natif (créé/modifié/supprimé) avec diff de champs, vue timeline et filtres (ressource/action/utilisateur)
Comptes étendusAUTH_USER_MODEL (modèle utilisateur personnalisé)extend!{} + AdminSite::accounts_users(ModelAdmin)
Verrouillage optimiste—natif — détecte les éditions concurrentes (__original_updated_at)
Protection brute-force logindjango-axes (tiers)natif — LoginGuard + rate limiter sur le login admin
Email création compteplugin tiersnatif — lien « choisir mon mot de passe » envoyé à l'adresse du compte, activé à la première définition
Rafraîchissement liste partiel—natif — list_partial HTMX sans rechargement page
Champs lecture seule / create-onlyreadonly_fields.readonly([...]) / .create_only([...]) sur le builder form
Fieldsetsfieldsets—
Navigation par datedate_hierarchy—
Exporttiers (django-import-export)—
Boutons save avancés"Save and continue" / "Save and add"—

Email

FonctionnalitéDjangoRunique
Envoi emailsend_mail() natifEmail::new().send() natif
Templates emailnatiftemplates Tera via .template("emails/mon.html")
Backend SMTPconfigurableconfiguration via .env
Backend dev (console)EMAIL_BACKEND = 'console'EMAIL_BACKEND=console dans .env

Observabilité / Logging

FonctionnalitéDjangoRunique
Config loggingdict LOGGING (logging Python)arbre RuniqueLog par domaine (forms, middleware, session, auth, admin, db, mailer, migration, templates, errors, builder) — chaque feuille un Option<LogLevel>
Tracing structurétiers (structlog)natif (tracing) ; les Result avalés loggent leur file:line via TraceResult::trace / trace_or
Sortieshandlers (console / fichier / …).output(...) : stdout couleurs, fichier roulant (JSON ou texte, non bloquant), LogSink custom (DB / HTTP / file)
Override fichier runtime—variable d'env RUNIQUE_LOG_FILE
Subscriber côté apple logging appartient à l'app.external() — Runique n'installe rien, émet quand même vers la façade tracing

Internationalisation

FonctionnalitéDjangoRunique
Langues supportéesillimitées9 langues par défaut (JSON compilé)
Fallbackouioui (Lang::En)
Traduction_("...")t("section.clé") / tf("...", &[var]) en Rust, textes du framework

Performance & Déploiement

AspectDjangoRunique
RuntimeCPython (interprété)Tokio async Rust (compilé)
Compilation—binaire unique
ACME / TLS autocertbot (externe)natif via feature acme

Ce qu'il manque encore (comparé à Django)

  • Redimensionnement automatique d'images : resize/cropping côté serveur non natif.
  • Vues CRUD publiques génériques : pas d'équivalent aux DetailView, ListView, CreateView de Django pour les vues publiques — envisagé via crud!{} (feuille de route, sans engagement). Le panel admin couvre le CRUD backoffice via ModelAdmin.
  • Signals / hooks modèles : before_save, after_save, before_delete, after_delete ne se déclarent pas dans model!{}. La structure interne HooksDef existe, sans lien avec le DSL ; le comportement généré par model!{} ne remplit que les colonnes auto_now. Prévu à la feuille de route, sans engagement.
  • Management commands : pas d'équivalent à manage.py custom_command — les opérations one-shot passent par src/bin/.
  • Test client intégré : pas de client HTTP de test natif. runique test et runique_test couvrent le lancement et l'isolation de la base ; les requêtes passent par reqwest ou tower::ServiceExt::oneshot.
  • Fixtures : pas de loaddata/dumpdata — les seeds sont des fonctions Rust.
  • Admin inline : pas d'édition d'objets liés directement dans le formulaire parent.
  • Admin actions de groupe custom : .action(...) ne supporte que des mises à jour de colonnes à valeur fixe — aucune logique Rust arbitraire possible sur la sélection (équivalent Django : actions avec une fonction quelconque).
  • Admin filtre FK : .filter([...]) ne supporte que les colonnes directes — pas de traversée de relation (article__auteur__nom).
  • Admin fieldsets : pas de groupement de champs par section dans les formulaires admin (fieldsets Django).
  • Admin navigation par date : pas de date_hierarchy pour filtrer la liste par année/mois/jour.
  • Admin export : pas d'export CSV/JSON natif depuis la liste.
  • Admin boutons save avancés : pas de "Enregistrer et continuer" / "Enregistrer et ajouter un autre".
  • i18n complète : t() et tf() sont des fonctions Rust pour les textes du framework. Pas de fonction de traduction dans les templates Tera, pas de pluralisation, et un projet ne peut pas encore ajouter ses propres traductions.
  • Tâches de fond : pas de framework de tâches ; Django en a un depuis la 6.0 (@task, avec un worker externe).
  • Sitemap / RSS : pas de génération native. .sitemap(url) ajoute seulement la directive Sitemap: au /robots.txt généré.
  • Authentification tiers : le mode Delegated nomme des fournisseurs (Google, Microsoft, Apple, LDAP, SAML, personnalisé), mais aucun flow n'est implémenté. JWT et clés d'API absents.
  • Observabilité sécurité : CSP report-uri/report-to absent, les violations ne sont pas collectées. Les connexions (réussies, échouées, verrouillages) passent par les logs (RuniqueLog) et ne sont pas enregistrées en base ; eihwaz_history trace les écritures de l'admin, les droits et les resets.

Limites connues

Base de données et migrations

LimiteDétail
Un moteur par compilation (sqlite, postgres, mysql)
SQLite : un seul écrivain à la fois, SQLITE_BUSY sous forte charge
Changer de moteur demande une base neuve : les données ne sont pas transférées
Base de données
Le changement de type d'une colonne n'est pas migré (--force écrit une migration vide, à compléter à la main)
Des snapshots copiés sans leurs migrations font croire que les tables existent
Identifiants SQL limités à 64 caractères sous MySQL / MariaDB
migration up : une migration n'est atomique que sous Postgres (SQLite, MySQL / MariaDB : une migration qui échoue peut rester à moitié appliquée)
Un arrêt brutal pendant makemigrations peut laisser des fichiers à moitié écrits (git pour restaurer)
Migrations, Makemigrations
search! : in (vec) avec un vec vide, pas de filtre conditionnel inlineRequêtes

Formulaires et messages

LimiteDétail
Un échec d'écriture en session perd le message flash sans erreurMacros flash
Un fichier publié puis une écriture en base qui échoue brutalement (coupure) peuvent laisser un fichier orphelinChamps

Sécurité, sessions, authentification

LimiteDétail
RateLimiter et LoginGuard n'ont pas de plafond mémoire
RateLimiter et LoginGuard comptent par processus : avec plusieurs instances, le quota est multiplié
Rate limiting
LoginGuard bloque par nom d'utilisateur : quelqu'un peut bloquer un compte dont il connaît le nomLoginGuard
Le store de sessions mémoire appartient à un processusStore de sessions
Un store personnalisé (with_session_store) ne peut pas être parcouru par utilisateur : un reset de mot de passe ne ferme pas ses sessionsSession
Mode Delegated : aucun fournisseur externe (OAuth, LDAP, SAML) n'est implémentéMots de passe
ACME : un seul site par machine (port 80 exclusif)Réseau

Admin

LimiteDétail
.links, .search_help, .empty_value acceptés mais sans effet sur l'affichageBuilder ModelAdmin
Pas de feuille de thème globale (contournement par un bloc)
.with_htmx est global, pas de fragment de liste par modèle
Un template de page ou une feuille de style absents ne sont signalés qu'à l'affichage
Surcharge
Pas d'interface de gestion des sessions—
Many-to-many : déclaré dans le DSL par une table pivot, sans gestion dans l'adminÉvolutions

Développement local

LimiteDétail
Les cookies ignorent le port : deux projets sur 127.0.0.1 partagent (et s'écrasent) le cookie de sessionDépannage

Solutions en attendant

LimiteSolution
RateLimiter et LoginGuard sans plafond mémoire, compteurs par processusLimiter aussi au reverse proxy (exemple ci-dessous)
LoginGuard bloque par nom d'utilisateurL'associer à une limite par IP, au reverse proxy ou avec RateLimiter
Store de sessions mémoire propre à un processusUne seule instance, ou un reverse proxy qui renvoie chaque visiteur vers la même (ip_hash avec nginx)
ACME : un seul site par machineConfier le TLS à un reverse proxy dès qu'il y a plusieurs sites
SQLite : un seul écrivain à la foisPostgreSQL pour une charge en écriture soutenue
Migration atomique seulement sous PostgreSQLSauvegarder la base avant migration up sur SQLite et MySQL / MariaDB
Changement de type de colonne non migréCompléter à la main la migration écrite par --force
Cookies communs à deux projets sur 127.0.0.1Ouvrir l'un sur 127.0.0.1, l'autre sur localhost

Limite par IP avec nginx, à adapter. La mémoire est plafonnée par la zone, et une requête refusée reçoit un code 503 par défaut :

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;
}

À venir

Sans date ni engagement : un bug ou une faille découverts passent avant les chantiers prévus.

  • Publication de la 4.0 : finalisation avant la mise en ligne sur crates.io, avec des tests sur des comptes à faibles droits et des groupes, et une revue OWASP complète.
  • Correctifs de sécurité (4.0.x) : plafonner le nombre de clés gardées en mémoire par RateLimiter et LoginGuard, et tester la protection contre les open redirects avec des charges connues de contournement d'URL.
  • 4.1, makemigrations : générer toutes les opérations de schéma sans étape manuelle : suppression de tables, création et suppression des tables de relation, respect de cascade, restrict et set_null, vrai ALTER lors d'un changement de type, détection des changements de valeur par défaut, et colonnes à la fois renommées et modifiées. Les noms de contraintes sont lus en base, et les changements destructeurs indiquent ce qu'ils touchent. La table pivot du many-to-many est générée depuis le DSL du modèle ; une table écrite à la main, avec des colonnes en plus, reste possible.
  • 4.2, base de données : get_or_create, update_or_create, exists(), last(), chargement des relations en une requête, méthodes d'accès du many-to-many et sa gestion dans l'admin. search! accepte aussi des groupes ET dans un OU, et la négation d'une condition ou d'un groupe, toujours avec des requêtes typées.
  • Tracing : finir la sortie fichier avec rotation, ajouter un identifiant de requête et ne laisser aucune erreur ignorée en silence. Les clés de traduction manquantes seront journalisées.
  • Langues ajoutées par un projet : permettre à un projet de fournir une langue absente de Runique, enregistrée par le builder.
  • Admin : filtres par date et par clé étrangère, navigation par date, fieldsets, champs en lecture seule, autocomplétion, édition inline, actions de groupe en Rust, export CSV/JSON et « enregistrer et continuer ».

Sans engagement : ces pistes remonteront quand un chantier se terminera.

  • Authentification : protection à deux niveaux contre la force brute, puis OAuth/OIDC, JWT, clés d'API, journal des connexions et rapports de violation CSP.
  • ORM et DSL : hooks de modèle, agrégats dans search!, index déclarés, bornes et paliers numériques.
  • Outils et stockage : vues CRUD publiques, client de test intégré, fixtures, commandes personnalisées, sitemap et RSS, redimensionnement d'images, et point d'extension de stockage pour les services compatibles S3.

Voir les problèmes connus par version