CustomBlock: ERP & Headless CMS
Framework-as-a-service con Server-Driven UI
Di cosa si tratta?
CustomBlock è un backend modulare e multi-tenant (B2B) che opera con un modello 'framework-as-a-service'. Un core condiviso gestisce l'autenticazione, i log di audit e la generazione della UI, mentre i singoli moduli tenant (come l'Headless CMS di questo portfolio o un gestionale ERP) si innestano fornendo solo i propri modelli dati — azzerando tutto il codice boilerplate.
La Sfida
L'obiettivo era costruire un'architettura multi-tenant con una rigida separazione tra il framework core e il codice del cliente, permettendo aggiornamenti zero-downtime tramite immagini Docker versionate. Volevo inoltre eliminare la scrittura manuale dell'HTML ingegnerizzando una Server-Driven UI capace di generare form, datagrid e API dinamicamente leggendo i metadati del database.
Architettura
Il codice del framework e quello dei tenant sono separati architetturalmente a ogni livello — nel sistema dei moduli, nell'immagine Docker e nella pipeline di deployment. Questo permette di aggiornare il framework su tutti i tenant rilasciando una nuova immagine Docker versioned, senza toccare il codice dei clienti.
- api/dynamic_crud.py
- auth/ & security/rbac.py
- ui/ (builder, components)
- models/ (User, AuditLog)
- gdpr.py · flash.py
- main.py (create_app)
- models.py
- router.py
- menu.py
- migrations/
- .env
Stack Tecnologico
| Linguaggio | Python 3.14 |
| Web Framework | FastAPI (async) |
| ORM / Driver | SQLAlchemy 2.x — aiomysql |
| Database | MySQL 8.4 |
| Template | Jinja2 — ChoiceLoader |
| UI Framework | Tabler + Tabulator.js |
| Autenticazione | JWT HS256 — HttpOnly cookies |
| Password Hashing | bcrypt (12 rounds) |
| Migrazioni | Alembic |
| Containerizzazione | Docker + Docker Compose |
| Configurazione | Pydantic v2 BaseSettings |
Dynamic CRUD Engine
register_crud_routes() genera 8 rotte standard per qualsiasi modello SQLAlchemy con una singola chiamata a funzione. Un cliente registra tutte le proprie risorse in un loop — l'intera interfaccia admin per 8 modelli richiede circa 10 righe di Python.
| Metodo | Rotta | Scopo |
|---|---|---|
| GET | /api/tabulator{path} | Endpoint JSON paginato per Tabulator.js |
| GET | {path} | Pagina lista con TabulatorComponent |
| GET | {path}/new | Form di creazione |
| POST | {path}/new | Crea record → flash message → redirect |
| GET | {path}/{pk} | Pagina di dettaglio in sola lettura (DetailComponent) |
| GET | {path}/{pk}/edit | Form di modifica con Delete + GDPR Anonymize opzionale |
| POST | {path}/{pk}/edit | Aggiorna record → flash message → redirect |
| POST | {path}/{pk}/delete | Elimina record → flash message → redirect |
Server-Driven UI Engine
Non si scrive HTML per ogni risorsa. La UI è espressa come un albero di dataclass Python nei metadati info={} delle colonne SQLAlchemy. Il motore legge questi metadati e produce pagine di amministrazione complete e interattive. Python non costruisce mai stringhe HTML — tutta la renderizzazione è delegata a Jinja2.
Albero dei Componenti
I metadati di colonna guidano tutto
Aggiungere una colonna al database con il dict info={} corretto aggiorna istantaneamente i form di creazione/modifica, la vista lista Tabulator e lo schema delle risposte dell'API pubblica — senza toccare nessun codice frontend.
Architettura della Sicurezza
JWT (HttpOnly)
Token HS256 in cookie HttpOnly, SameSite=Lax. Login dual-mode: i client browser ricevono un redirect Set-Cookie, i client API ricevono {"access_token": ...} JSON — dallo stesso endpoint.
RBAC
Factory function che restituisce dependency FastAPI-injectable. Guard preconfigurati: allow_admin, allow_authenticated. Gli item di navigazione sono filtrati lato server per ruolo — nessun dropdown vuoto per utenti con accesso limitato.
Audit Log Automatico
Event hook SQLAlchemy (after_insert, before_update, before_delete) catturano silenziosamente ogni modifica tramite ContextVar. Le entry vengono scritte nella stessa transazione DB della modifica — nessun log fantasma in caso di rollback.
Anonimizzazione GDPR
Motore a 5 step per l'Art. 17: anonimizza i campi PII, elimina la storia di audit del record target, anonimizza le azioni storiche dell'utente e scansiona tutti i log per l'email del soggetto. Attivato tramite un bottone con conferma modale nel form di modifica.
Tenant Attivi
Otto modelli interconnessi: Prodotti, Categorie, Magazzini, Fornitori, Ordini d'Acquisto, Clienti, Ordini di Vendita. Tre dropdown di navigazione. L'intera interfaccia admin è registrata in circa 10 righe di Python.
Otto modelli multilingua (IT/EN). API pubblica con language flattening lato server, filtro bozze, risoluzione URL assoluti e endpoint dedicato per il download CV cross-origin. Alimenta questo sito portfolio.
Architettura Docker
Il Dockerfile copia solo core/ — le directory dei clienti sono deliberatamente escluse. Il risultato è un'immagine versioned immutabile cb-core:<versione>. Il .env di ogni cliente fissa il proprio CORE_VERSION per rollout graduali. Due clienti in esecuzione simultanea non condividono nulla: reti separate, volumi separati, database separati.
cbenv.sh — Developer CLI
Sorgente nella shell (source cbenv.sh), espone il comando cbdev. Un controllo pre-avvio verifica che l'immagine cb-core richiesta esista in locale prima di avviare qualsiasi stack — nessun pull silenzioso o errore a metà avvio.
-
cbdev build <version>— Costruisce un'immagine cb-core versioned dal Dockerfile locale -
cbdev <customer> start— Renderizza il template Compose e avvia lo stack del tenant -
cbdev <customer> migrate— Esegue alembic upgrade head dentro il container -
cbdev <customer> bash— Apre una shell dentro il container dell'app in esecuzione -
cbdev <customer> remove— Ferma lo stack ed elimina i suoi volumi (distruttivo)