Hassler Platform
Un runtime personnel durable : un VPS unique, un reverse proxy lisible, des services conteneurisés, PostgreSQL comme cœur, un monolithe modulaire par domaines. La complexité n'entre que lorsqu'elle résout une douleur présente, répétée et observable. Trois mantras gouvernent chaque revue : disposabilité future (tout doit pouvoir être remplacé), composition, simplicité et façon canonique (on n'invente pas, on recherche).
Cette page est le carnet de bord ; la source de vérité est
le markdown du repo — docs/blueprint.md (vision),
docs/adr/ (décisions), docs/runbook/journal.md
(chaque intervention serveur, sans exception),
docs/runbook/commandes.md (manuel de survie sans LLM).
| App | Quoi | État |
|---|---|---|
chat | messagerie familiale temps réel (WS), pièces jointes, comptes | en prod |
files | explorateur de fichiers en arborescence, stockage CAS sha256 | en prod |
irish | explorateur de musique irlandaise (23 130 tunes TheSession), partitions, MP3, mode travail DSP | en prod |
contacts | annuaire de la plateforme : personnes et entreprises, téléphones/adresses multiples, autocomplete BAN — le socle des documents Typst | en prod |
miam | découverte publique bars/restaurants façon swipe (ex-table) — données OpenStreetMap synchronisées par ville, serving PostGIS local (autour de soi), carte enrichie (horaires, adresse, équipements, mini-carte), fiches sponsorisées labellisées dans le deck | en prod |
admin | backoffice : comptes, santé des services, observabilité stockage (accès URL directe, jamais dans le hub) | en prod |
docs | cette page | en prod |
louison / hello | pages statiques historiques | en prod |
hub hassler.fr | bureau de la plateforme : fenêtres redimensionnables + snap, dock, apps embarquées en iframe | en prod |
02Topologie actuelle
Trois ports publics (22, 80, 443). Les conteneurs n'écoutent qu'en loopback : seul Caddy est joignable depuis Internet.
flowchart LR I((Internet)) --> DNS[DNS OVH
hassler.fr] DNS --> FW[UFW
80 / 443] FW --> C[Caddy
HTTPS auto] C -->|hello.hassler.fr| H["/var/www/hello
statique, sur l'hôte"] C -->|louison.hassler.fr| L[conteneur louison
127.0.0.1:3100] C -->|docs.hassler.fr| D[conteneur docs
127.0.0.1:3101] C -->|hassler.fr + hub.hassler.fr| HB[hub
127.0.0.1:3106] C -->|chat.hassler.fr| CA[chat-app
127.0.0.1:3102] C -->|chat.hassler.fr/ws| CW[chat-ws
127.0.0.1:3103] C -->|files.hassler.fr| FA[files-app
127.0.0.1:3104] C -->|irish.hassler.fr| IR[irish-app
127.0.0.1:3105] C -->|contacts.hassler.fr| CO[contacts-app
127.0.0.1:3107] CA -->|INSERT + NOTIFY| PG[(postgres
127.0.0.1:5432)] CW -->|LISTEN| PG FA --> PG FA --> FB[/opt/hassler/data/files
CAS sha256, bind mount/] IR --> PG CO --> PG IR --> S3[(S3 hassler-media
GRA, MP3)] subgraph VPS["VPS-3 · Ubuntu 26.04 · Gravelines"] FW; C; H subgraph Docker["Docker Compose · /opt/hassler"] L; D; CA; CW; FA; IR; CO; PG end FB end
Côté repo : monorepo Turborepo (bun = gestionnaire de
paquets, Node 22 = runtime), apps en Hono + Drizzle côté serveur, React 19
+ Vite en Feature-Sliced Design côté web. Les primitives UI vivent dans
@hassler/ui (thin wrappers React Aria), les tokens dans
@hassler/design-system — règles dans DESIGN.md.
flowchart LR
subgraph Apps["apps/"]
HW[hello]; LO[louison]; DO[docs]; CH[chat + chat/web]; FI[files + files/web]; IRA[irish + irish/web]; HUB[hub]
end
subgraph Packages["packages/"]
UI["ui
React Aria thin wrappers"]
DS["design-system
tokens.css"]
TO["tooling
eslint · tsconfig"]
end
CH --> UI --> DS
CH --> DS
FI --> UI
FI --> DS
IRA --> UI
IRA --> DS
CTA[contacts + contacts/web]
CTA --> UI
CTA --> DS
HUB --> UI
HUB --> DS
Apps -.-> TO
03Les apps, en vrai
chat.hassler.fr — la messagerie familiale
Messages persistés en PostgreSQL, temps réel par un processus
WebSocket dédié (chat-ws) découplé de l'app HTTP : le serveur
insère puis NOTIFY, le WS écoute (LISTEN) et
pousse l'id seul — les clients relisent en HTTP. Pièces jointes
(images inline, pdf/zip génériques, preview plein écran), présence en
ligne, indicateur de saisie, backoffice admin (création de comptes,
réinitialisation de mots de passe). Auth Better Auth, sessions cookie,
pas d'inscription publique.
files.hassler.fr — l'explorateur de fichiers
Arborescence de dossiers/fichiers en base ; les octets vivent dans un CAS sha256 (content-addressed storage) monté en bind mount sur l'hôte — inspectable sans Docker. Upload jusqu'à 520 Mo (borne Caddy), dédoublonnage par contenu. À venir : sélection multiple, drag & drop, appel depuis le chat (trombone).
irish.hassler.fr — la musique irlandaise
Le projet passion. Données publiques TheSession (licence ODbL, attribution en pied de page ; clause spécifique : le contenu des tunes ne passe jamais par un LLM, y compris en debug) :
| Donnée | Volume | Usage |
|---|---|---|
| tunes | 23 130 | recherche floue pg_trgm (nom + 29 319 alias), filtres combinables type × tonalité, tri popularité (tunebooks) |
| settings (partitions ABC) | 54 750 | gravure abcjs, pager entre variantes, incipit au survol des listes, ABC brut copiable, édition de variantes perso |
| pistes d'albums | 138 334 (7 894 albums) | discographie par tune ; page album dédiée + drag & drop de MP3 sur les pistes |
| bookmarks | miroir du compte thesession | page dédiée, resynchronisée à chaque déploiement |
| MP3 famille | bucket S3 privé | lecture streaming (URL présignée) + mode travail |
Le mode travail est l'outil du musicien : ralentir
sans changer la hauteur, transposer sans changer le tempo, boucler une
phrase (points A/B) — DSP signalsmith-stretch (WASM/
AudioWorklet) chargé paresseusement, piloté par une machine à
états XState v5 explicite (première brique applicative de
l'ADR-0004) : le cycle de vie Web Audio vit dans des acteurs, React ne
fait que rendre des snapshots. Le mode travail remplace le
player natif quand il s'ouvre.
La passe du 2026-07-12 — irish prend la grammaire de miam : navigation basse (Explorer / Signets / Outils, badge du nombre de signets), listes en carte arrondie (bord à bord sur mobile), recherche pleine hauteur, rangées entièrement cliquables. Le mode travail gagne une forme d'onde (crêtes calculées du buffer déjà décodé, dessin canvas aux couleurs du thème) : cliquer = se déplacer, la boucle A→B devient une région ombrée aux poignées draggables sous l'onde, la vitesse passe en select et la hauteur (±12 demi-tons) se réinitialise d'un bouton. Les signets se trient « ajoutés récemment » (le rang du flux thesession est désormais persisté), une variante préférée s'étoile et s'ouvre par défaut, des liens YouTube s'attachent aux morceaux, et les albums affichent enfin leurs pistes absentes de thesession, en retrait. Nouvel onglet Outils : métronome (ordonnanceur « Two Clocks » précis au sample, tap-tempo, accessible aussi depuis un morceau) et accordeur (micro sans filtres voix, détection MPM, jauge en cents, la ajustable) — ensemble, le clic se coupe pendant l'accordage pour ne pas fausser l'aiguille.
Ingestion : script idempotent (upsert par ids thesession), relancé explicitement — comme les migrations, jamais implicite. Prochain chantier : sets « souvent joués ensemble » et recherche mélodique par notes.
contacts.hassler.fr — l'annuaire (2026-07-06)
Le socle du futur courrier Typst (ADR-0011). Un contact est une personne physique OU une entreprise (raison sociale, SIRET, forme, APE, TVA — l'en-tête d'un devis n'est pas celui d'une lettre), et n'est pas forcément un utilisateur — l'annuaire est premier, un compte peut y être lié. Téléphones et adresses multiples, avec un « préféré » par type garanti par la base (index partiels UNIQUE — deux clics concurrents font un 409 propre, jamais deux préférés). Les adresses se saisissent avec l'autocomplete de la BAN (api-adresse.data.gouv.fr — publique, gratuite, sans clé), champs éditables à la main. Annuaire familial partagé : toute la famille lit et écrit ; aucune donnée de contact ne passe dans les logs.
miam.hassler.fr — découvrir où manger
App autonome de découverte restaurants/bars/cafés. OpenStreetMap sert
de base d'amorçage : sync régionale vers PostGIS local,
recherche floue et fiches partageables. Les données produit enrichies
(nom canonique, description, menu, ardoise, claims propriétaires,
sponsorisé) vivent dans des tables Miam séparées des colonnes OSM.
Le backoffice plateforme peut observer Miam, mais Miam ne dépend pas des
users Better Auth de chat ou admin.
Depuis le 2026-07-12, le deck Découvrir sert des fiches sponsorisées (deux campagnes de démo aux assets fictifs) : panneau plein cadre inversé, badge « Sponsorisé » toujours visible, annonceur affiché, CTA vers son site en nouvel onglet. Insertion aléatoire plafonnée par graine stable (max 2 par deck, jamais dans les 3 premières cartes, ≥ 6 cartes organiques entre deux) ; une sponsorisée n'entre jamais dans la sélection (la règle vit dans la factory du deck, pas dans l'UI) et l'undo ne la ramène pas. Depuis le 2026-07-13 (PR9, ADR-0030), les campagnes sont entièrement administrables depuis admin.hassler.fr : création (uuid serveur, cible https obligatoire), édition champ par champ, poids, fenêtre de diffusion, pause — sans suppression (une campagne se désactive, elle garde sa trace). Le statut affiché dit la vérité du serving (en diffusion / programmée / expirée / en pause), pas le seul interrupteur. Les images arriveront avec le stockage média S3.
hassler.fr — le hub, un bureau dans le navigateur
L'apex sert un bureau : chaque app s'ouvre dans une
fenêtre (iframe) depuis un dock. Les fenêtres sont
déplaçables et redimensionnables (8 poignées, chacune
ancrée sur le côté opposé), se snappent aux quatre côtés
et aux quatre coins (demis et quarts) ou en plein écran, et se minimisent
vers le dock. La fenêtre active passe au premier plan au clic.
Deux responsabilités séparées, selon l'ADR-0004 : l'état persistant
(collection de fenêtres, géométrie, empilement) vit dans une
factory jotai — les fenêtres sont des
atoms-in-atom (splitAtom), chaque fenêtre ne relit
que le sien ; le geste transitoire (drag, resize) est une
machine à états XState v5 qui ne fait que committer le
rect dans les atoms.
z
par fenêtre, et minimiser masque en display:none sans
démonter l'iframe. De même, le snap se calcule sur les
bords de la fenêtre face au viewport, pas sur le curseur
(ce que font les gestionnaires de fenêtres) — sinon un curseur qui frôle
un bord snappe par accident.Le hub est aussi la vitrine du design system : le
sélecteur de thème du dock propose cinq skins (système, clair, sombre, un
thème pqp navy+orange et un thème aurora teal, avec
glow) qui ne font que repeindre les tokens @hassler/design-system.
Le même sous-menu Thème se retrouve dans un menu kebab
présent dans chaque app (chat, irish, files) — construit sur le composant
Menu + SubmenuTrigger de @hassler/ui
(React Aria), aux côtés des nouvelles briques partagées
(CopyButton, Link/Router).
04Déploiement & CI
Le repo est la source de vérité. Une commande :
./infra/scripts/deploy.sh. Jamais d'édition à la main sur le
serveur. Chaque push passe le quality gate GitHub Actions (lint, types,
build, steiger) ; chaque déploiement réussi est enregistré comme
Deployment GitHub (onglet Environments : quoi, quand).
Les migrations SQL sont un pas explicite du script,
toujours relues avant.
sequenceDiagram participant M as Mac (repo git) participant V as VPS /opt/hassler participant D as Docker participant C as Caddy M->>V: rsync compose.yaml + apps/ + Caddyfile V->>D: docker compose build + migrations explicites D-->>V: images immuables, conteneurs up V->>C: caddy validate + reload C-->>M: HTTPS 200 sur chaque sous-domaine M->>M: gh api : Deployment enregistré
Cache HTTP (depuis le 2026-07-05) : index.html
servi en no-cache (revalidation systématique),
assets hashés Vite en immutable un an — plus jamais de
« vieux bundle » après un déploiement.
05Réseau, DNS & TLS
Une seule machine exposée, un seul point d'entrée chiffré, et un domaine dont on ne touche jamais la partie courrier.
Le chemin d'une requête
Le navigateur résout irish.hassler.fr via le DNS d'OVH,
obtient l'IP du VPS, ouvre une connexion TLS sur le port 443. Caddy
termine le HTTPS (certificats Let's Encrypt renouvelés seuls) et, selon le
nom annoncé dans la poignée de main TLS (le SNI), relaie vers le
bon conteneur en loopback. Aucun conteneur n'est joignable directement :
seuls 22, 80 et 443 sont ouverts.
La carte DNS (zone hassler.fr)
| Nom | Type | Cible | Rôle |
|---|---|---|---|
hassler.fr (apex) | A / AAAA | VPS (51.38.177.41 · IPv6) | le hub — le bureau de la plateforme |
www | CNAME | → apex | redirigé en 301 vers l'apex par Caddy |
chat / files / irish / docs… | A | VPS | une app par sous-domaine |
@ | MX | mx1/2/3.mail.ovh.net | courrier OVH — jamais touché |
autoconfig / autodiscover | CNAME | mailconfig.ovh.net | réglages mail auto — intacts |
Bascule de l'apex vers le hub (2026-07-05)
Le domaine racine servait un ancien site (source sur GitHub, rien à
perdre) ; il pointe désormais sur le hub. Règle d'or d'une bascule sans
coupure : le vhost avant le DNS. On a donc (1) ajouté le
bloc Caddy hassler.fr et déployé, (2) repointé les
enregistrements A/AAAA de l'apex vers le VPS,
(3) rechargé Caddy pour émettre le certificat une fois le DNS en place.
Les enregistrements MX et mail n'ont pas bougé — la
messagerie continue sans interruption.
dig hassler.fr peut renvoyer longtemps l'ANCIENNE IP à cause
du cache du résolveur local. Pour trancher, on interroge un serveur
faisant autorité (dig +short NS hassler.fr puis
dig hassler.fr @<ns>) ou un résolveur public
(@8.8.8.8, @1.1.1.1) — eux voient la vérité de la
zone, pas le cache de la machine.06Stockage & sauvegardes
Trois lieux de vie pour les octets, deux buckets S3, un principe : la sauvegarde ne dort jamais au même endroit que la donnée.
| Donnée | Où elle vit | Comment elle survit |
|---|---|---|
| Messages, comptes, arbres files, tunes | PostgreSQL 18 (volume pgdata) |
pg_dumpall nightly → restic |
| Photos du chat | volume Docker chatfiles |
restic nightly |
Fichiers du domaine files |
CAS sha256, bind mount /opt/hassler/data/files |
restic nightly |
| MP3 d'irish | bucket S3 hassler-media (GRA, privé, versionné) |
versioning du bucket — réplication GRA→RBX à instruire |
Secrets hors-git (.env, credentials S3) |
fichiers 600 sur le VPS | restic nightly + gestionnaire de mots de passe |
restic (ADR-0008) : dépôt chiffré de bout en bout sur le
bucket hassler-backups à Roubaix — un autre
site que le VPS (Gravelines). Timers systemd : sauvegarde à 03:30,
vérification d'intégrité le dimanche (+ relecture de 5 % des octets la
1re semaine du mois). Rétention 7 quotidiennes / 4 hebdo / 12 mensuelles.
Test de restauration réussi le 2026-07-04 (sha256
identiques, base rechargée dans un PostgreSQL jetable). Kit de survie :
le repo git, les accès OVH, le mot de passe restic du gestionnaire — rien
d'autre. Toutes les commandes : docs/runbook/commandes.md.
Les tunes d'irish sont des données publiques re-générables : l'ingestion des dumps TheSession est idempotente, la synchro des bookmarks tourne à chaque déploiement.
07Sécurité
- Surface réseau minimale : UFW n'ouvre que 22/80/443 ; aucun conteneur n'expose de port sur 0.0.0.0 (loopback uniquement), PostgreSQL compris.
- Auth : Better Auth, sessions serveur en cookies
durcis (
__Host-, Secure), pas d'inscription publique — les comptes se créent depuis le backoffice du chat. Rate limiting activé. - Secrets : jamais dans le repo ni dans une conversation ; fichiers 600 sur le VPS, transmis par scp, copies locales détruites. Les retours d'API de credentials ne sont jamais affichés bruts (leçon apprise — on filtre par noms de champs).
- En-têtes durcis (revue du 2026-07-06) : HSTS
partout ; CSP
frame-ancestors— les apps ne sont embarquables que par le hub, tout le reste refuse l'iframe. - Sauvegardes chiffrées de bout en bout (restic), bucket versionné contre un VPS compromis.
- DNS : seuls les sous-domaines de la plateforme sont touchés ; MX et enregistrements mail intouchables.
08Incidents & leçons
Le journal complet vit dans
docs/runbook/journal.md. Les leçons qui ont changé les règles :
index.html servi sans
Cache-Control → les navigateurs gardaient un vieil index en
cache heuristique, donc de vieux bundles ; (2) dans une liste flex
scrollable, des rangées portant overflow: hidden voient leur
min-height tomber à 0 (spec flexbox) — les longues listes
étaient écrasées à 0px. Règles : en-têtes de cache explicites partout ;
shrink-0 sur les items de liste flex ; et reproduire dans
un vrai navigateur avec les vraies données avant de conclure
(diagnostic final fait en pilotant Chrome par MCP : mesures de rects,
pas de spéculation).recording_id, migration
relue à la main. Règle : toujours vérifier la cardinalité réelle d'un
dump avant de choisir la clé primaire.useQueryStates), les queries react-query clées dessus.docs/review/. Règle confirmée : ce
qui n'est pas revu adversarialement n'est pas fini.--frozen-lockfile casse à l'apparition d'un nouveau
workspace. Vérifié deux fois à nos dépens.session, mais
chaque sous-domaine garde son cookie __Host-<app>.
Le proxy admin peut relayer une valeur de session vers
chat/files/irish sous le nom attendu
par l'app cible. Miam est l'exception : admin vérifie
l'humain, puis appelle Miam via un secret serveur-à-serveur pour préserver
l'autonomie future du domaine.09Roadmap
Livré récemment : le hub hassler.fr
(fenêtres, snap, dock, cinq thèmes), irish v2 (incipit au survol,
albums + drag & drop de MP3, ABC copiable, édition de variantes,
et depuis le 2026-07-07 une popularité en étoiles
honnête — classification head/tail breaks, les quintiles s'écrasaient
sur la loi de puissance), l'annuaire de contacts
(personnes, entreprises ET administrations — CAF/CPAM/impôts seedés
depuis l'annuaire officiel — le socle des courriers Typst), et une
grammaire de listing unifiée dans toutes les apps :
recherche fine avec loupe-spinner, raccourcis de filtre, compteur.
Le backoffice centralisé a son premier étage : la CLI
hassler (login, stats fichiers par uploadeur, liste des
comptes) qui valide le contrat /admin avant une app
d'administration dédiée (accès par URL directe, jamais dans
le hub) — laquelle montrera aussi la santé de chaque service (dots
colorés). Décidé et acté (ADR-0013) : les
facettes publiques — des dossiers
public/Desktop1…N de files projetés sur les plans du hub,
chaque facette étant un explorateur en grille façon Finder (vignettes,
navigation, fil d’ariane), avant l’animation cube. Les
facettes sont en ligne : hassler.fr ouvre sur un
bureau public plein écran (dock persistant, chevrons, pastilles de
plan), et les médias — images, vidéos, sons — s’ouvrent dans un
visualiseur commun partagé par le chat, le hub et
files. À venir : l’app d’administration (gestion des
comptes + observabilité des uploads/médias/fichiers en tables ET
graphes, stockage par emplacement, purge des orphelins, pastilles de
santé — ADR-0012), chat v2
(salles multiples + messages directs — ADR-0014), la suite
miam propriétaires (revue des claims, espace manager,
édition de fiche : description, horaires, galerie, menu, ardoise,
événements — le formulaire public de revendication enregistre déjà,
personne ne le relit encore ; les fiches sponsorisées, elles, sont
en prod depuis le 2026-07-12 — ADR-0028/0029),
les documents Typst (courriers depuis l’annuaire), le launchpad du dock,
les sets irish et la recherche mélodique.
flowchart LR A[1-5
VPS · SSH
firewall]:::done --> B[6
Caddy
HTTPS]:::done B --> C[7-8
Docker
1re app]:::done C --> D[9
PostgreSQL]:::done D --> G[16-18
chat
realtime]:::done G --> F[12
files]:::done F --> E[10-11
irish]:::done E --> ALB[albums · DnD
édition ABC]:::done ALB --> HUB[hub hassler.fr
fenêtres · snap · thèmes]:::done HUB --> ADM[backoffice
CLI hassler]:::done ADM --> V2[à venir
app admin · facettes publiques
chat v2 · restos PostGIS] F --> H[19-23
observabilité] H --> I[24-30
IA · agents
documents Typst] classDef done fill:#3f8e5c,stroke:#2e6b45,color:#fff
10Décisions d'architecture
ADR-0001 · OVH VPS-3accepté
6 vCores, 12 Go, 100 Go NVMe à Gravelines. Le stockage est une
ressource fondatrice (domaine files) et l'upgrade OVH est
un aller simple : autant couvrir toute la roadmap d'emblée pour
~3 €/mois de plus que le VPS-2.
ADR-0002 · Ubuntu Server 26.04 LTSaccepté
Meilleure documentation serveur + guides OVH + support jusqu'en 2031. Debian 13 écartée de peu ; la fraîcheur de la 26.04 est un risque faible pour un socle headless minimal.
ADR-0003 · Monorepo privé GitHubaccepté
Un seul repo pour apps, capacités, infra et docs : le pendant
naturel du monolithe modulaire, et le meilleur contexte pour les
agents de code. Extraction possible plus tard via
git filter-repo.
ADR-0004 · Orchestration agents + XStateproposé
Fable orchestrateur, Opus codeur, modèles routés par complexité. Côté applicatif : XState v5 comme moteur de workflows — la machine à états route et valide, le LLM comprend, jamais l'inverse. Première brique applicative livrée le 2026-07-05 : la machine du lecteur de travail d'irish.
ADR-0005 · Stack TS : Hono · Drizzle · WS découplé · React/FSDaccepté
Bun (install) + Node (runtime), Turborepo. Serveur WebSocket
séparé de l'app, bus PostgreSQL LISTEN/NOTIFY (l'événement porte
l'id, les clients relisent en HTTP). Front React/Vite en
Feature-Sliced Design. Depuis 2026-07 : primitives UI en package
(@hassler/ui, React Aria) + tokens
(@hassler/design-system) — quarantaine Base UI levée.
ADR-0006 · Auth : Better Auth, sessions cookieaccepté
Le standard TS 2026 (l'équipe Auth.js le recommande). Sessions serveur en cookies durcis (position OWASP), pas d'inscription publique — comptes créés par l'admin. SSO multi-apps différé.
ADR-0008 · Sauvegardes : restic → OVH Object Storage RBXaccepté
Timer systemd sur le VPS (pas de serveur dédié) : pg_dumpall
+ volumes + secrets hors-git, chiffrés par restic vers le bucket
hassler-backups à Roubaix — un autre site que le VPS
(Gravelines), versioning S3 activé contre un VPS compromis. Mot de
passe restic copié hors-VPS obligatoirement. Test de restauration
réel réussi le 2026-07-04.
ADR-0009 · irish : app dédiée, données TheSessionaccepté
App à part entière (pas un module du chat) : Hono + Drizzle +
journal de migrations séparé (__drizzle_migrations_irish —
jamais de journal partagé entre services d'une même base). Données
publiques TheSession sous ODbL : attribution affichée, contenu jamais
traité par un LLM (clause explicite du dump).
ADR-0010 · Ingestion idempotente des dumpsaccepté
Les ids thesession sont les clés primaires : la ré-ingestion est un upsert pur, les liens profonds restent stables. Remplacement en bloc pour les tables satellites (alias, recordings, bookmarks). Lancement explicite, comme les migrations. Le script ne logge que des comptes, jamais des contenus.
ADR-0011 · Domaine contacts — annuaire de la plateformeaccepté
Un domaine dédié (app Hono + web FSD, port 3107, migrations propres) plutôt qu'une annexe du backoffice chat : user ≠ contact, personne OU entreprise (bloc juridique pour les devis), canaux et adresses 1-N avec « préféré » garanti par index partiels UNIQUE, autocomplete BAN. Écartés : package partagé, Redis/Mongo du rapport, adresse JSONB. Accepté et livré le 2026-07-06.
ADR-0012 · Backoffice centralisé — /admin par domaineaccepté
Chaque domaine expose SES endpoints /admin derrière un
middleware requireAdmin vérifié serveur (pattern Django « admin
déclaré par module, surface unique »). Trois consommateurs
successifs : CLI hassler (livrée — login par session,
l’API-key attendra better-auth 1.7), app admin fait-main
(Recharts pour les stats, accès par URL directe uniquement,
jamais dans le hub, URL non publiée), serveur MCP en dernier.
Médias agrégés en SQL, jamais de scan du storage.
ADR-0013 · Dossiers publics → facettes du hubaccepté
Table de projection public_desktop (une ligne = un
dossier publié, jetable par DROP) + seed Desktop1..3 ; publier =
déplacer un fichier dans public/DesktopN. Première
surface API anonyme de la plateforme : GET-only, anti-traversal
ltree, allowlist de colonnes, CORS restreint à hassler.fr, noindex.
Sur le hub : un explorateur lecture seule en grille façon Finder
(vignettes, nom dessous, fil d’ariane en haut), plans coulissants
d’abord, cube ensuite.
ADR-0014 · Chat v2 — salles + messages directsaccepté
Les DM sont des salles ordinaires à 2 membres (unicité de la
paire EN BASE — leçon Matrix), le salon actuel devient la « salle
famille » automatique et non-quittable. Non-lus par curseur
last_read_at, un seul canal NOTIFY avec le
roomId dans le payload, liste Discussions en une
requête LATERAL. Gouvernance famille : chacun crée des salles et
ouvre des DM, pas de rôles par salle. Correctif intégré : les
pièces jointes se resserrent par appartenance à la salle.
ADR-0015 · Miam, ex-table — bars/restaurantsaccepté
Découverte publique façon swipe, données OpenStreetMap jamais
appelées à la requête : synchronisation par ville (cron + admin),
serving 100 % PostGIS local (geography + index
GIST + ST_DWithin). Ville à deux modes d’extraction
(Narbonne-Plage n’a pas de polygone administratif). Cinq amenities
(restaurant, fast-food, café, bar, pub), favoris en localStorage,
filtres « ouvert maintenant » + cuisine. ODbL : une colonne = un
seul monde, jamais de donnée maison dans une colonne OSM.
ADR-0029 · Miam admin autonome, enrichi et sponsoriséaccepté
Miam reste extractible : tables métier, owners/managers et future
auth appartiennent au domaine Miam. admin.hassler.fr est
un backoffice plateforme consommateur HTTP : il vérifie l'admin
connecté, puis appelle les endpoints Miam avec un secret interne
MIAM_ADMIN_INTERNAL_TOKEN. Aucun cookie Better Auth admin
n'est relayé vers Miam. Le futur admin Miam couvrira carte régionale,
sync, fiches enrichies, claims propriétaires, analytics agrégés et
cartes sponsorisées clairement labellisées.
ADR-0030 · Miam : le backoffice écrit via le pontaccepté
Le pont admin→miam passe de la lecture à l'écriture de contenu (campagnes, puis assertions établissements, médias, couverture) sans bouger la frontière : chaque route relayée est énumérée une par une (la liste des relais EST la liste des pouvoirs du backoffice), la validation appartient au domaine (zod strict, verdicts 400/404 remontés tels quels), les ids créés par l'admin sont des uuid serveur, pas de DELETE en v1. Les médias iront sur un bucket S3 dédié en upload direct navigateur ; la couverture s'étendra par département avec dry-run obligatoire.
ADR-0016 · Service outils statelessaccepté
Un seul service pour les transformations sans état : Typst → PDF (courriers/devis/factures/CV depuis l’annuaire) et optimisation d’images (AVIF primaire + WebP fallback via sharp — recherche 2026 : AVIF ~20 % plus léger que WebP, 93-95 % de support). Tous deux CPU-bound et purs (entrée → artefact, aucun état). Service interne (loopback, pas de sous-domaine public), appelé serveur-à-serveur (files à l’upload, documents, CLI, MCP). Bornes : taille max, timeout, concurrence.
11Jargon
Le vocabulaire de la plateforme, en clair — pour relire ce carnet sans rien tenir pour acquis.
| Terme | Ce que c'est |
|---|---|
| Apex (racine) | Le domaine nu hassler.fr, sans sous-domaine devant. |
| Sous-domaine | Un préfixe : chat.hassler.fr, irish.hassler.fr. Une app chacun. |
| A / AAAA | Fait pointer un nom vers une IP — A en IPv4, AAAA en IPv6. |
| CNAME | Un alias : « ce nom est le même qu'un autre ». Interdit de le mélanger avec un A/AAAA sur le même nom. |
| MX | Mail eXchange : quels serveurs reçoivent le courrier du domaine. On n'y touche jamais. |
| TTL | Time To Live : combien de temps un résolveur garde une réponse DNS en cache. |
| Résolveur / autorité | Le résolveur (FAI, 8.8.8.8…) répond depuis son cache ; les serveurs faisant autorité (OVH ici) détiennent la vérité de la zone. |
| Reverse proxy | Un portier (Caddy) qui reçoit toutes les requêtes et les distribue aux bons services derrière. |
| TLS / HTTPS / ACME | TLS chiffre la connexion (le cadenas). ACME est le protocole par lequel Caddy obtient et renouvelle seul les certificats Let's Encrypt. |
| SNI | Server Name Indication : le nom de domaine annoncé au début de la poignée de main TLS, qui permet à Caddy de choisir le bon certificat. |
| Loopback | L'adresse 127.0.0.1, joignable seulement depuis la machine elle-même — nos conteneurs n'écoutent que là. |
| CAS | Content-Addressed Storage : un fichier rangé sous l'empreinte (sha256) de son contenu — même contenu = même emplacement, dédoublonnage gratuit. |
| Migration | Un script versionné qui fait évoluer le schéma de la base ; joué explicitement au déploiement, jamais en douce. |
| Idempotent | Rejouable sans dégât : relancer l'ingestion des tunes deux fois donne le même résultat qu'une fois. |
| ODbL | La licence des données TheSession : attribution obligatoire, et le contenu des morceaux ne passe jamais par un LLM. |
| FSD | Feature-Sliced Design : l'organisation du front en couches (app → pages → widgets → features → entities → shared). |
| Machine à états (XState) | Un modèle explicite « états + transitions » ; le cœur du lecteur de travail d'irish. La machine décide, le reste obéit. |
| Service-to-service | Un appel serveur interne entre deux apps. Pour Miam admin : admin-app vérifie l'admin, puis appelle miam-app avec un secret technique. |