genro-asgi · documento 1 di 3

Il server: tutto quello che un applicativo trova già montato

Scrivi il metodo una volta e lo esponi come API REST, come strumento per un agente e come comando. Intorno, già pronto, tutto ciò che un applicativo reale finisce sempre per doversi costruire da sé.

Il punto di partenzaPerché un altro server

Un framework web classico lega il routing al protocollo: la funzione decorata è un endpoint HTTP, e se domani la stessa logica deve rispondere a un agente conversazionale o a un comando da terminale, la riscrivi.

genro-asgi parte dalla separazione opposta. Il motore di routing vive nelle tue classi applicative e non sa nulla di HTTP; il server è un adattatore di trasporto che mappa richieste esterne su voci del router.

Definisci gli handler una volta. Il protocollo è una vista su di essi, non la loro forma.

Da qui discende quasi tutto il resto: lo stesso albero di metodi diventa una API REST con il suo schema OpenAPI, un server MCP per gli agenti, e una CLI, senza duplicare una riga. E sopra questa base c'è montato tutto ciò che serve davvero: autenticazione con più metodi, sessioni, task pianificati e batch, storage astratto, configurazione dichiarativa, documentazione generata.

Per che cosa è stato costruito

Vale la pena dirlo subito, perché evita confronti fuori bersaglio. genro-asgi serve benissimo una API che riceve, risponde e dimentica — ed è quello che questo documento descrive. Ma non nasce lì: nasce dagli applicativi gestionali, dove una pagina resta aperta per ore, accumula contesto di lavoro, riceve aggiornamenti quando i dati cambiano e dialoga col server in continuazione.

È un caso d'uso che le architetture pensate per API senza stato affrontano male, e che ha prodotto scelte precise — l'utente come chiave di distribuzione del carico, la pagina come unità di stato vivo — raccontate nel secondo documento. Chi valuta genro-asgi per una API pura trova un server completo; chi lo valuta per un gestionale trova la ragione per cui esiste.

Come leggere questo documento

Descrive il server mono-processo nella sua forma compiuta: ciò che genro-asgi mette a disposizione di chi scrive un'applicazione. La roadmap in appendice dichiara, voce per voce, cosa è già operativo oggi e cosa è in corso di implementazione.

Il piano di orchestrazione — lo stato vivo per utente e per pagina, i processi multipli — è materia del secondo documento, che spiega anche perché quella scelta è stata fatta.

# hello.py — un server completo
from genro_asgi import AsgiServer, RoutedApplication
from genro_routes import route


class Hello(RoutedApplication):
    mount = ""          # questa app risponde alla radice del sito

    @route()
    def index(self) -> dict[str, str]:
        return {"hello": "world"}

    @route()
    def greet(self, name: str = "world") -> dict[str, str]:
        return {"hello": name}


if __name__ == "__main__":
    server = AsgiServer(applications=[Hello()])
    server.serve(host="127.0.0.1", port=8000)

Da qui hai già: le due rotte, il loro schema OpenAPI, la documentazione navigabile, l'app di sistema /_server con login e gestione utenti, il registro delle richieste e il pool per il lavoro bloccante. Nessuna configurazione richiesta.

uvicorn AsgiServer il server È l'app ASGI catena middleware errori → CORS → auth → sessione smistamento 1° segmento → mount applicazione il suo albero di rotte handler sync → pool, async → loop risposta buffer · flusso · SSE

Il cuoreLe routes

Ogni classe che eredita da RoutingClass possiede un router e uno solo, creato automaticamente ed esposto come proprietà route. Lo stato è isolato per istanza: due istanze della stessa classe hanno due router indipendenti, il che rende naturale servire la stessa classe due volte sotto nomi diversi.

Un metodo entra nel router con @route(). Il decoratore accetta un nome esplicito, un identificatore globale per la risoluzione inversa, un tipo di media per il risultato, e qualunque metadato aggiuntivo che i plugin sapranno leggere.

# gerarchie: si collega un'istanza già costruita
class UsersAPI(RoutingClass):
    @route()
    def list(self):
        return ["alice", "bob"]

class Application(RoutingClass):
    def __init__(self):
        self.users = UsersAPI()
        self.add_branches({"name": "users", "instance": self.users})

app = Application()
app.route.node("users/list")()      # ["alice", "bob"]
app.route.nodes()                  # l'albero, introspezionabile a runtime

Dichiarate dal codice, costruite quando servono

Quali rotte esistono lo dice il codice, mai un dato a runtime: il routing non è un registro mutabile. Ma l'albero non viene materializzato tutto all'avvio — un ramo prende vita alla prima volta che qualcuno lo attraversa, così un'applicazione con centinaia di rami paga solo quelli che vengono davvero visitati.

Introspezione

nodes() restituisce l'albero con firme, metadati e schemi. È ciò che alimenta la generazione di OpenAPI, la faccia MCP e qualunque interfaccia di amministrazione tu voglia costruire.

Schemi di risposta automatici

L'annotazione di ritorno — un TypedDict, una dataclass — viene convertita in JSON Schema e pubblicata nei metadati della rotta. Documenti il tipo, non lo schema.

Più trasporti

L'adattatore HTTP è genro-asgi; nel pacchetto delle routes ce n'è già un altro che costruisce una gerarchia di comandi da terminale dalla stessa classe.

Sopra le routesI plugin e la configurazione a runtime

Un plugin si aggancia alla pipeline del router in due punti: quando una rotta viene dichiarata e quando un handler viene invocato. I plugin si ereditano lungo la gerarchia, così un ramo figlio riceve quelli del padre senza ridichiararli. Cinque arrivano nella scatola.

Validazione e schemi

Il plugin pydantic costruisce il modello dalla firma del metodo: valida gli argomenti in ingresso e produce lo schema che finisce nella documentazione. In genro-asgi è struttura fissa, armata su ogni router — quindi le firme sono sempre catturate.

Autorizzazione a tag

Ogni rotta può dichiarare una regola sui tag dell'utente, con una piccola algebra booleana:

  • | almeno uno dei tag
  • & tutti i tag
  • ! nessuno dei tag
  • () raggruppamento

Capacità d'ambiente

Una rotta può richiedere una capacità dell'ambiente — un servizio raggiungibile, una libreria presente. Le capacità si accumulano lungo il cammino nella gerarchia: un ramo figlio eredita quelle del padre e aggiunge le sue.

Filtro per canale

La stessa rotta può essere visibile da un canale e invisibile da un altro — accessibile via MCP ma non via HTTP pubblico, per esempio. I canali si dichiarano anche per pattern.

Logging

Tracciamento delle invocazioni con controllo per singola rotta, attivabile e disattivabile senza toccare gli handler.

Configurazione a runtime

Le impostazioni dei plugin si applicano a caldo, globalmente o per singolo handler, con caratteri jolly sui nomi. La stessa chiamata sa anche riferire lo stato invece di cambiarlo, così puoi interrogare la configurazione viva.

Perché conta

Autorizzazione, validazione e visibilità non sono if dentro gli handler: sono proprietà dichiarate della rotta, leggibili dall'esterno. È la stessa lettura che genera la documentazione — quindi ciò che il documento dichiara e ciò che il server applica non possono divergere.

La configurazioneIl sistema delle grammatiche

La configurazione di genro-asgi non è un dizionario di stringhe né un file YAML da indovinare: è una grammatica. Ogni sezione è un elemento dichiarato con la sua firma — parametri, tipi, valori di default, quali figli ammette e in che cardinalità. Scrivere una configurazione è comporre quella grammatica in Python, e un errore di composizione è un errore al boot, non un KeyError in produzione sei mesi dopo.

from genro_asgi import AsgiServer
from genro_asgi.config import AsgiConfigBuilder

class ServerConfiguration(AsgiConfigBuilder):
    def main(self, root):
        cfg = root.configuration()
        cfg.server(host="127.0.0.1", port=8000)

server = AsgiServer(config=ServerConfiguration)

Ogni classe porta la propria grammatica

La grammatica non è centralizzata: è un attributo della classe che la parla. Il server ha la sua, l'applicazione base ha la sua, il sottosistema dei task ha la sua — e quando monti un'applicazione, la sua grammatica entra per riferimento nella configurazione del sito. Chi scrive una nuova applicazione dichiara le proprie sezioni e le ottiene validate come tutte le altre, senza toccare il core.

Le sezioni che trovi già

SezioneCosa dichiara
serverhost, porta e i parametri di runtime; contiene session (durata) e tasks
applicationsle applicazioni montate, per codice, con l'applicazione predefinita; ognuna innesta la grammatica della propria classe
authenticationpassword di amministrazione, archivi di utenti e credenziali, politica di login, provider OIDC, credenziali su header
middlewarequali middleware armare e con quali parametri
databasesi database montati, per codice
storagei punti di montaggio dello storage, con la chiave di cifratura quando serve
pluginsi plugin del router, abilitabili singolarmente con le loro opzioni
openapititolo, versione e descrizione del documento generato

I valori esterni si risolvono quando li leggi

Un valore che viene dall'ambiente non è una stringa copiata al boot: metti un resolver dove andrebbe il valore, e si risolve alla lettura. Il runtime consuma sempre il valore corrente, e non esiste il caso in cui la configurazione materializzata perde in silenzio un livello intero.

Il server legge se stesso

Non c'è un componente esterno che costruisce un server da una configurazione: il server costruisce la propria porta di lettura e le chiede i valori che gli servono. La porta è pubblica e interrogabile per percorso — server.config("server.host") — su quattro livelli in cascata: il valore scritto, il default dichiarato nella firma dell'elemento, il default passato dalla chiamata, e infine un errore esplicito. Un'applicazione legge il proprio sottoalbero con app.config(percorso): possiede un indirizzo nell'albero, mai una copia di una sua fetta.

Gli argomenti passati esplicitamente al costruttore vincono su quelli configurati, uno per uno. È la regola che rende --host e --port della riga di comando semplici argomenti, senza calcoli di precedenza da nessuna parte.

Il modelloServer e applicazioni

Un server possiede il runtime: il loop, la catena dei middleware, il pool per il lavoro bloccante, il ciclo di vita, il registro delle richieste, e l'insieme delle applicazioni che serve. Un'applicazione possiede il comportamento e non sa nulla di porte o middleware.

Ogni applicazione porta due metà della propria identità: il codice, che è il suo nome, e il mount, che è il prefisso di URL sotto cui risponde. Sono distinti, quindi la stessa classe può essere servita due volte sotto nomi diversi. Un mount vuoto è la radice del sito.

Lo smistamento ha una sola regola, uguale su ogni server: primo segmento del percorso verso l'applicazione montata lì; altrimenti quella sulla radice; altrimenti, per / con un default dichiarato, un redirect; altrimenti 404.

Le classi che erediti

ClasseCosa aggiunge
BaseApplicationil contratto minimo: identità, mount, riferimento al server, agganci di ciclo di vita
RoutedApplicationinnesta il motore di routing: i metodi @route diventano la superficie dell'app
OpenApiApplicationaggiunge REST, lo schema OpenAPI e le pagine di documentazione
McpApplicationespone lo stesso albero come server MCP per gli agenti
McpOpenApiApplicationentrambe le facce sullo stesso albero di rotte
ServerApplicationl'app di sistema, montata da sola su ogni server

Le capacità si compongono

Il server spedito impila su una base snella tutte le capacità come mixin cooperativi: comunicazione, autenticazione, sessioni, middleware, plugin, storage, task. Le capacità esistono sempre — non aggiungi una capacità sottoclassando, la alimenti passandole configurazione. Non esiste l'attributo che è None finché un flag non lo accende.

L'app di sistema, senza configurarla

Ogni server monta da sé un'applicazione di sistema sotto /_server: login e logout, elenco dei metodi di accesso disponibili, gestione degli utenti, gestione delle credenziali emesse, e la superficie completa dei task. Ha il suo schema OpenAPI come ogni altra applicazione. È automatica, non configurata: un server costruito a mano ce l'ha esattamente come uno costruito da una configurazione.

DocumentazioneOpenAPI, generato dall'albero vero

Lo schema non è un file da mantenere allineato: è la traduzione dell'introspezione del router. Le firme catturate dal plugin di validazione diventano parametri e corpi; le annotazioni di ritorno diventano schemi di risposta; i metadati dichiarati sulla rotta diventano riassunti, tag e codici.

Ogni applicazione che eredita da OpenApiApplication pubblica il proprio documento e le pagine di documentazione navigabile sotto il proprio sottoalbero. Il controllo è anche per singolo handler: metodo HTTP, inclusione o esclusione, dettagli della voce.

Poiché i controlli per voce sono armati su ogni router come struttura fissa, valgono sempre — non c'è la configurazione dimenticata che lascia una rotta fuori dal documento senza che nessuno se ne accorga.

AgentiMCP sulla stessa superficie

Model Context Protocol è il modo in cui un modello linguistico scopre e invoca strumenti. In genro-asgi è una faccia, non un'applicazione parallela: il motore parla JSON-RPC 2.0 sopra lo stesso router che serve la API REST, con il trasporto Streamable HTTP senza stato.

La conseguenza pratica è che un metodo decorato una volta è insieme un endpoint REST e uno strumento per l'agente. Lo schema che l'agente riceve per capire come chiamarlo è lo stesso schema che finisce nella documentazione — generato dalla stessa introspezione.

Il filtro per canale governa cosa l'agente vede: una rotta può essere dichiarata accessibile via MCP e non altrove, o viceversa, senza duplicare l'albero.

IdentitàAutenticazione: il catalogo

Un applicativo reale non ha un modo di autenticare: ne ha diversi, per interlocutori diversi — il browser di una persona, il processo che chiama la API, l'agente, il provider aziendale. genro-asgi li porta tutti, convergenti sullo stesso risultato.

Password, con blocco progressivo

Login in due rotte distinte — una JSON per il client, una HTML per il browser — mai un fiuto dell'intestazione dentro l'handler. Il contatore dei fallimenti vive sul record dell'utente, quindi sopravvive ai riavvii ed è condiviso fra processi. Dopo N fallimenti l'attesa cresce esponenzialmente; i tentativi rifiutati durante il blocco non lo prolungano, così un attaccante non può tenere fuori un utente legittimo.

OIDC, un provider per istanza

Flusso authorization-code con PKCE sempre attivo, quindi funziona anche senza segreto client. La scoperta dei metadati del provider è pigra e memorizzata: un server che parte mentre il provider è irraggiungibile non muore. Lo stato è legato alla sessione e verificato al ritorno; il token d'identità è validato per firma, destinatario ed emittente. Ogni provider configurato porta la propria identità e i propri tag.

Credenziali su intestazione

Basic, bearer e JWT, dichiarati nella sezione di configurazione e consumati dal nucleo di autenticazione. L'intestazione vince sempre sulla sessione: una chiamata autenticata è quello che dichiara di essere, indipendentemente dal cookie che porta con sé.

Chiavi API revocabili

Un registro di credenziali bearer emesse, ognuna revocabile singolarmente, con contratto astratto e implementazione su file già pronta — sostituibile con la tua senza toccare i consumatori.

Autorizzazione a tag

L'identità autenticata porta i suoi tag, e ogni rotta dichiara la regola booleana che li interroga. Le sezioni di sistema sono protette così: la gestione di utenti, credenziali e task è dietro un cancello di amministrazione dichiarato, non dietro un controllo scritto a mano in ogni handler.

Un solo tipo di identità

Comunque tu sia entrato — password, OIDC, token — il risultato è un avatar: l'unico tipo di identità autenticata del pacchetto. Chi consuma l'identità non sa da quale porta sia entrata, e non deve saperlo.

Aggiungere un metodo di login significa scrivere una classe che si descrive: come si chiama, che tipo è, e se possiede rotte proprie. Se ne possiede, entrano nell'albero sotto la superficie di sistema; se non ne possiede — il caso della password — vive nel registro ordinato che alimenta l'elenco dei metodi disponibili, senza inquinare l'albero di routing con nodi vuoti.

Il limite di frequenza per indirizzo IP è materia di un middleware dedicato, non del gestore di login: il blocco progressivo difende un'identità dai tentativi ripetuti, il limite per IP difende il server da chi lo martella, e sono due difese con vittime diverse.

ContinuitàSessioni

Una sessione tiene lo stato di un visitatore tra una richiesta e l'altra: un identificatore, i suoi metadati, un albero di dati liberamente strutturato e una collezione di avatar per chiave.

La decisione che vale la pena conoscere è come si comporta al login: la sessione è un continuum, e il login la arricchisce di un'identità senza sostituirla. L'identificatore non cambia, i dati non si perdono — il carrello che un visitatore anonimo ha riempito sopravvive all'accesso — e il cookie che il client già possiede resta valido. Non esiste un cookie emesso al momento del login: se ne emette uno solo quando nasce una sessione anonima nuova.

La difesa contro la fissazione della sessione sta a monte, nella disciplina del cookie — non leggibile da script, con vincolo di provenienza, e il token mai letto dall'URL — invece che in una rotazione di identificatore che costerebbe la continuità.

Lo store è un contratto: quello in memoria è il predefinito, il tuo si innesta senza che nulla intorno cambi.

Lavoro differitoTask, pianificazione e batch

Il sottosistema dei task copre due bisogni che di solito richiedono due dipendenze esterne: eseguire qualcosa più tardi o periodicamente, ed eseguire un lavoro lungo mantenendone traccia.

La pianificazione: tre forme

FormaCome si scriveSemantica
every"30s" · "15m" · "2h" · "1d"a intervallo fisso dall'ultima esecuzione
croni classici cinque campi, con * , - /semantica di sistema, valutata in ora locale — analizzata in casa, senza dipendenze aggiuntive
atuna lista di istanti ISOognuno scatta una volta sola; esaurita la lista non c'è più una prossima esecuzione

Nessun recupero del passato: la prossima esecuzione si calcola sempre in avanti da adesso, mai riempiendo a ritroso il tempo in cui il server era spento.

Lo spool: lo stato è la posizione

Un task batch è una cartella, e il suo stato è dove quella cartella si trova nell'albero. Chi lo lancia la crea sotto pending/; il gestore la sposta nella cartella del worker assegnato; il worker la esegue e la sposta in terminated/ o aborted/. Una transizione di stato è uno spostamento — atomico, su un solo punto di montaggio.

Non ci sono lock, perché la sicurezza è strutturale: un solo scrittore per ogni directory di stato, e prendere in carico un lavoro è una rinomina atomica. Dentro la cartella vivono il descrittore, i parametri della chiamata, l'ultimo stato di avanzamento scritto dal worker, il risultato, e un file-marcatore che segnala la richiesta di annullamento.

Un identificatore di batch è terminale: non si riprende e non si rilancia da sé. Un lavoro rimasto orfano perché il suo worker è morto viene chiuso come interrotto al riavvio successivo; rilanciarlo significa un lavoro nuovo, con un identificatore nuovo. Non esiste lo stato ambiguo in cui non sai se qualcosa stia ancora girando.

Il termometro: l'avanzamento che si guarda mentre accade

Un lavoro che dura dieci minuti ha bisogno di raccontarsi, altrimenti l'utente non sa distinguere «sta lavorando» da «è morto». Il codice del batch pubblica il proprio avanzamento — a che punto è, su quanto, con che messaggio — e ogni pubblicazione fa due cose in un colpo solo: scrive lo stato su disco, accanto al lavoro, e lo diffonde sul canale vivo verso chi lo ha lanciato.

La doppia natura è deliberata, e risolve il problema che di solito costringe a scegliere. Il file è la verità: chi ricarica la pagina, chi arriva dopo, chi vuole sapere a che punto era un lavoro di ieri legge quello. Il canale vivo è la cortesia: chi sta guardando adesso vede la barra muoversi senza interrogare il server a intervalli. Se il canale non c'è — nessuno sta guardando — la scrittura avviene comunque, e non si perde niente.

Interrompere un lavoro

Un batch lungo va potuto fermare, e il modo in cui lo si ferma non è indifferente. Uccidere il processo a metà lascia le cose a metà: una transazione aperta, un file scritto per un terzo, un contatore incrementato per righe che non esistono.

Quindi l'interruzione è cooperativa. Chi la chiede posa un segnale accanto al lavoro; il codice del batch lo controlla ai propri punti di respiro — tipicamente a ogni ciclo, dove sa di essere in uno stato consistente — e quando lo trova chiude ordinatamente e si dichiara interrotto. Il lavoro finisce nella cartella degli interrotti, con la sua storia leggibile.

Chi chiede lo stop non ammazza: chiede. È il batch che sa dove può fermarsi senza lasciare macerie.

Il costo di questa scelta è che un batch che non guarda mai il segnale non si ferma — ed è un costo accettato, perché l'alternativa è un sistema che promette una cancellazione pulita e non può mantenerla.

La superficie di gestione

Sotto la sezione di sistema, protetta dal cancello di amministrazione, trovi già gli endpoint per l'intero ciclo: elencare le pianificazioni, crearne, modificarle, abilitarle e disabilitarle, forzarne l'esecuzione immediata, cancellarle, leggerne i log; e per i batch: elencare lo spool filtrando per proprietario e stato, leggere l'avanzamento, chiedere l'interruzione, recuperare il risultato.

Sono endpoint con il loro schema OpenAPI come ogni altra rotta, quindi l'interfaccia di amministrazione è un consumatore come un altro — e le illustrazioni che seguono mostrano che forma può prendere.

L'esecuzione può stare in-processo, sul pool del server stesso, oppure essere affidata a processi dedicati: il modello — spool, stati, ciclo di vita — è lo stesso nei due casi, e quale dei due si usi è una questione di dispiegamento descritta nel secondo documento.

La videata dei task

_server · task pianifica esegui ora IN ESECUZIONE Ricalcolo giacenze avviato 14:02 · m.rossi · magazzino/ricalcolo 73% 14.600 di 20.000 · articolo AC-8841 interrompi ~2 min Export fatture Q3 avviato 14:09 · g.bianchi · contabilita/export 18% 1.100 di 6.200 · composizione PDF interrompi ~9 min Invio solleciti avviato 13:58 · pianificato · crediti/solleciti 96% interruzione richiesta · attende il punto di respiro richiesto PIANIFICAZIONI Chiusura giornaliera cron · 0 2 * * * ultima 02:00 · ok prossima 02:00 Sincronia listini every · 15m ultima 14:00 · ok prossima 14:15 Riconciliazione banca at · 3 istanti ultima 09:30 · errore disabilitata

Illustrazione teorica, non uno screenshot: mostra quali informazioni la superficie di sistema già espone. I termometri vengono dal canale vivo, il resto dagli endpoint dello spool e delle pianificazioni. Il terzo lavoro ha l'interruzione richiesta e sta finendo il proprio ciclo — lo stato intermedio che una cancellazione cooperativa rende visibile invece di nascondere.

La videata degli utenti

_server · utenti nuovo utente IDENTITÀ TAG ACCESSO ULTIMO INGRESSO STATO m.rossi@acme.it magazzino · operatore password oggi 08:14 attivo g.bianchi@acme.it contabilita · superadmin OIDC · azienda oggi 09:02 attivo l.verdi@acme.it vendite password ieri 17:41 bloccato 4′ integrazione-erp servizio · sola lettura chiave API oggi 14:11 attivo L.VERDI · 5 TENTATIVI FALLITI il contatore vive sul record dell'utente, quindi il blocco è già valido su ogni processo · azzeramento manuale disponibile

Gli stessi endpoint alimentano l'elenco: identità, tag che governano l'autorizzazione, metodo con cui la persona entra, e lo stato del blocco progressivo — che essendo scritto sul record dell'utente è già coerente fra processi, senza un archivio condiviso di appoggio.

PersistenzaStorage, database e forma dei dati

Lo storage è un'astrazione con punti di montaggio simbolici: il codice scrive su un nome logico — «il mount dei documenti, cartella fatture» — e dove quel nome atterri lo decide la configurazione. Non è zucchero sintattico: è ciò che permette allo stesso codice, non ricompilato e non ramificato, di lavorare su una cartella locale mentre sviluppi e su un archivio remoto in produzione.

Disco locale e disco di rete, senza accorgersene

La trasparenza copre due mondi che di solito richiedono due codici diversi.

Il primo è il filesystem, e comprende già i dischi di rete: un volume condiviso montato dal sistema operativo — un NAS aziendale, una condivisione di rete — per il processo è un percorso, quindi il mount lo raggiunge senza sapere che è remoto. Il secondo è l'archiviazione a oggetti e tutto ciò che si raggiunge per protocollo: bucket S3, Google Cloud Storage, archivi in memoria per i test. Cambia una riga nella configurazione, non una riga nel codice.

Questo è il motivo pratico per cui il modello vale la pena: il cliente che tiene i documenti sul proprio NAS e quello che li tiene su un bucket usano la stessa applicazione, e la differenza vive in un file di configurazione invece che in un ramo del codice da mantenere due volte.

Le capacità si dichiarano, non si scoprono sbattendoci

La trasparenza ha un limite onesto: i backend non sanno fare le stesse cose. Un bucket non sa aggiungere in coda a un file esistente; un disco locale non sa produrre un collegamento firmato a scadenza; un archivio di sola lettura non accetta scritture.

Invece di far finta che siano equivalenti — e lasciare che la differenza esploda in produzione — ogni backend dichiara le proprie capacità, diciannove in tutto: lettura, scrittura, cancellazione, elenco, metadati, collegamenti firmati e pubblici, atomicità, ricerca posizionale, aggiunta in coda, ottimizzazione della copia lato server, impronta nei metadati, sola lettura, temporaneità. Il codice può chiedere prima di provare.

E la dichiarazione non è un'etichetta scritta a mano: per un bucket, la capacità di gestire versioni viene verificata interrogando il bucket vero. Se il versioning è attivo su quel bucket la capacità è vera; se non lo è, è falsa. Non c'è un file di configurazione che possa mentire su un fatto verificabile.

Il multiversione

Tre delle diciannove capacità riguardano le versioni, e sono separate perché descrivono cose diverse: tenere la storia di un file, elencarla, e accedere a una versione precisa.

Dove il backend le possiede, un file non è un contenuto ma una storia di contenuti: si può chiedere a un nodo quali versioni abbia — con la loro data e il loro identificatore — e aprirne una vecchia per leggerla, senza toccare quella corrente. Sovrascrivere non distrugge: aggiunge.

Un allegato sostituito per errore è un problema di recupero, non di backup, se lo storage sotto tiene la storia.

Il valore per l'applicazione è che non deve costruirsi un sistema di versioni proprio — la tabella delle revisioni, i nomi con il suffisso progressivo, la cartella dello storico — quando il supporto sa già farlo. E dove il supporto non lo sa, la capacità risponde no: l'applicazione lo scopre interrogando, non con un file perduto.

La cifratura è un'opzione del montaggio, non una libreria da orchestrare a mano: si dichiara sul mount che deve essere protetto, e il codice che scrive non cambia.

Database

I database si dichiarano nella loro sezione, per codice, e il core espone il contratto minimo di un database montato: quel che serve al server per possederlo e passarlo alle applicazioni che lo chiedono.

Le fondamenta condivise

Bag — l'albero di dati

Una struttura ad albero ordinata, con attributi sui nodi, osservabile: si può sottoscrivere e sapere cosa è cambiato. È la forma in cui viaggiano configurazione, dati di sessione e stato vivo delle pagine.

TYTX — tipi che sopravvivono al trasporto

Un decimale spedito da Python arriva come decimale, non come stringa da riconvertire a mano. Un solo protocollo su tre formati — JSON, XML e MessagePack — e passare dall'uno all'altro è un parametro, non una riscrittura.

Builders — le grammatiche

Il sistema che rende la configurazione una grammatica dichiarata invece che un dizionario: elementi con firma, cardinalità dei figli, validazione alla composizione.

Routes, storage, toolbox

Il motore di routing, l'astrazione di persistenza e le utilità comuni. Sei pacchetti in tutto, ognuno pubblicato e testato per conto proprio.

Le stesse strutture esistono lato browser

Qui c'è una scelta che si vede solo quando si guarda l'intero percorso di un dato, dal database alla pagina. Bag e TYTX non sono soltanto librerie Python: esistono anche in JavaScript. Bag ha il proprio pacchetto per il browser; TYTX porta le due implementazioni nello stesso repository ed è pubblicato su entrambi i registri, quello Python e quello npm.

La conseguenza è che un albero di dati costruito sul server e lo stesso albero manipolato nella pagina hanno la stessa forma e la stessa semantica. Non c'è un modello di dominio di qua e un oggetto JSON appiattito di là, con il livello di traduzione in mezzo che va tenuto allineato a mano e che è il posto dove si annidano metà dei difetti di integrazione.

Il dato non cambia forma attraversando il confine fra i due linguaggi. Quel confine, di solito, è dove le informazioni si impoveriscono.

Vale in particolare per i tipi. Una data resta una data, un decimale resta un decimale con la sua precisione — il caso degli importi, dove passare per un numero in virgola mobile è un errore che si manifesta molto più tardi, in una somma che non torna di un centesimo. E vale per la struttura: gli attributi sui nodi, l'ordine dei figli, la possibilità di osservare cosa è cambiato sono gli stessi da entrambe le parti.

Un'equivalenza verificata, non dichiarata

Due implementazioni della stessa cosa divergono, appena qualcosa non le obbliga a restare uguali — è la stessa lezione che governa i due trasporti del motore di smistamento. Per questo TYTX porta una suite di prove attraverso i due linguaggi: un lato genera i dati, l'altro li rilegge e verifica di ottenere esattamente quello che ci si aspetta, nelle due direzioni. L'equivalenza è una proprietà provata a ogni build, non una promessa nel readme.

MessagePack quando il volume lo chiede

Che il formato sia un parametro e non un'architettura ha una conseguenza pratica sul canale fra pagina e server. Finché il traffico è modesto, JSON va benissimo ed è ispezionabile a occhio, il che in fase di sviluppo vale molto. Quando il dialogo si fa fitto — ed è esattamente ciò che accade con un canale aperto, come racconta il secondo documento — lo stesso identico contenuto può viaggiare in MessagePack: forma binaria, più compatta e più rapida da comporre e leggere.

Il codice applicativo non cambia di una riga, perché i tipi sono descritti dal protocollo e non dal formato. Si sceglie in base alla stagione del progetto: leggibile mentre si sviluppa, compatto quando si misura.

Le basiHTTP, flussi e trasporti

Richiesta e risposta sono due classi piatte, senza gerarchie da imparare. La richiesta legge il corpo in anticipo e lo offre già interpretato; la risposta conosce il protocollo tipizzato, quindi un decimale o una data attraversano il confine senza conversioni manuali.

Per i flussi lunghi c'è la risposta a blocchi, e sopra di essa gli eventi inviati dal server già inquadrati — la via naturale per l'avanzamento di un lavoro o per una notifica.

Le eccezioni HTTP si sollevano da qualunque punto del codice e trovano risposta nel middleware più esterno: non passi codici di stato lungo la catena delle chiamate.

La catena dei middleware

Sette middleware pronti — errori, CORS, autenticazione, sessione, log di accesso, filtro dei percorsi di sonda — ordinati da un numero, non da una gerarchia di classi: il più piccolo è il più esterno, e inserire il tuo significa scegliere una posizione. Solo la gestione degli errori è attiva di default; le altre si armano quando configuri la capacità corrispondente.

Due trasporti, un solo motore

Accanto a HTTP il server parla WSX — WebSocket eXtended: un protocollo che porta la semantica HTTP sopra un canale WebSocket, così una chiamata che arriva dal socket trova lo stesso albero di rotte, gli stessi plugin e la stessa autorizzazione di una richiesta HTTP.

La regola che governa questa parte è che il motore di smistamento è uno solo, con due trasporti innestati. Non due motori paralleli: due motori divergono, perché nulla li obbliga a restare uguali, e ogni funzionalità aggiunta a uno va poi ritrovata nell'altro. Il contesto della chiamata, gli agganci sulla risoluzione della rotta e le pulizie dopo il servizio esistono identici su entrambi i trasporti, e una sola batteria di test di contratto li verifica insieme.

La stretta di mano del socket passa dal controllo di provenienza, come ogni altra difesa del server: un canale aperto è una porta, e vale la stessa disciplina delle altre.

OsservabilitàMetriche in formato Prometheus

Il monitor di sistema risponde alla domanda «cosa sta succedendo adesso»: quali lavori girano, chi è collegato, quali processi sono vivi. È fatto per una persona che guarda.

Le metriche rispondono a una domanda diversa — «come si è comportato questo sistema nell'ultima settimana, e cosa è cambiato da quando abbiamo rilasciato» — e il destinatario non è una persona ma un raccoglitore. Per questo il server espone un endpoint in formato Prometheus, che è di fatto lo standard con cui ogni sistema di raccolta sa parlare.

Quello che espone non è una traduzione di quel che si vede nel monitor: sono le grandezze che ha senso guardare nel tempo.

Il traffico

Richieste servite, distinte per applicazione montata e per esito, e la distribuzione dei tempi di risposta — non la media, che nasconde esattamente i casi che interessano.

Il lavoro bloccante

Occupazione del pool di thread: quanti stanno lavorando e quanti esistono. È la grandezza che dice se il server è al limite prima che i tempi di risposta comincino a peggiorare.

Identità e sessioni

Sessioni vive, accessi riusciti e falliti per metodo — password, OIDC, credenziali su intestazione — e identità attualmente sotto blocco progressivo. Una curva di accessi falliti che si impenna è un fatto operativo, non solo una nota di sicurezza.

I task

Lavori nelle rispettive posizioni dello spool — in attesa, in corso, terminati, interrotti — durata di quelli che finiscono e ritardo delle pianificazioni rispetto al loro istante previsto. Un ritardo che cresce è la prima avvisaglia di un sistema che non sta al passo.

Un livello, tre in tutto

Questo è il primo dei tre livelli di osservabilità. Quello descritto qui è il processo che serve: espone se stesso, e su un'installazione mono-processo è tutto ciò che serve. Il secondo livello — il pool di processi con lo stato vivo — e il terzo — la distribuzione fra domini e macchine — sono nei documenti successivi, e sono costruiti sullo stesso formato: chi raccoglie non deve imparare tre linguaggi.

Messa in esercizioDalla riga di comando

Un file di configurazione più un comando sono un'unità di deployment completa. Il comando risolve quello che gli passi in tre passi: un'assegnazione che nomina direttamente una classe applicativa, un percorso a un file esistente, oppure il nome di un server già registrato.

I server registrati vivono in un registro che è un archivio di puntatori, mai una copia: tiene la stringa sorgente e le opzioni date, quindi rilanciare per nome esegue sempre il codice corrente. Un file di pid non viene mai creduto sulla parola — mancante, illeggibile o riferito a un processo che non c'è più si leggono tutti come «non in esecuzione», così un server crollato risulta fermo e non fantasma.

C'è la ricarica automatica per lo sviluppo. La supervisione multi-processo, invece, è deliberatamente fuori: appartiene a uno strumento dedicato della famiglia.

AppendiceRoadmap: dove siamo

Il documento descrive il server nella sua forma compiuta. Questa appendice è il solo posto dove si legge lo stato di avanzamento, e l'unico che cambia mentre l'implementazione procede.

Il pacchetto è coperto da 1.567 test con una copertura del 96%.

CapitoloStatoNota
Routing, gerarchie, introspezioneoperativo
Plugin del router e configurazione a runtimeoperativovalidazione, autorizzazione a tag, capacità, canale, logging
Grammatiche e configurazioneoperativotutte le sezioni, resolver, porta di lettura a quattro livelli
Server, applicazioni, smistamentooperativo
App di sistema _serveroperativologin, utenti, credenziali, task
OpenAPI e documentazioneoperativo
MCPoperativo
Autenticazione: password con blocco progressivooperativo
Autenticazione: OIDC con PKCEoperativoun provider per istanza
Autenticazione: basic, bearer, JWT, chiavi APIoperativo
Sessioni e storeoperativo
Task: pianificazione e spooloperativotre forme di pianificazione, superficie di gestione completa
Task: esecuzione su processi dedicatiin implementazioneil modello è completo, l'esecuzione in-processo funziona; la distribuzione riusa la supervisione del documento 2
Bag e TYTX anche in JavaScriptoperativostesse strutture e stessi tipi da entrambe le parti · JSON, XML e MessagePack come formati intercambiabili · equivalenza verificata da una suite di prove attraverso i due linguaggi
Storage e databaseoperativomount simbolici · filesystem e dischi di rete, fsspec (S3, GCS, memoria) · 19 capacità dichiarate per backend, versioning verificato interrogando il supporto · cifratura per mount
HTTP, flussi, SSE, middlewareoperativo
Trasporto WSXoperativosecondo trasporto del motore unico: stesso albero di rotte, stessi plugin, stessa autorizzazione di HTTP
Limite di frequenza per IPin implementazionemiddleware dedicato; il blocco progressivo per identità è già operativo
Termometri e interruzione cooperativa dei batchoperativoavanzamento su file e sul canale vivo in un colpo solo; il segnale di stop e la sua verifica
Metriche in formato Prometheusin implementazionele grandezze sono già misurate e leggibili dagli endpoint di sistema; manca l'endpoint nel formato che i raccoglitori consumano
Riga di comando e registro dei serveroperativo

Ultimo aggiornamento della roadmap: 13 agosto 2026. Le voci del corpo del documento non cambiano quando una riga di questa tabella passa a «operativo».