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.

core/
  • api/dynamic_crud.py
  • auth/ & security/rbac.py
  • ui/ (builder, components)
  • models/ (User, AuditLog)
  • gdpr.py · flash.py
  • main.py (create_app)
customers/<name>/
  • models.py
  • router.py
  • menu.py
  • migrations/
  • .env
Aggiornamenti senza modifiche: Aggiornare il framework significa cambiare CORE_VERSION nel .env del tenant e riavviare lo stack. Non è mai richiesta alcuna modifica al codice del cliente.

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

BaseComponent (ABC) ├── CardComponent — Tabler card wrapper (title + body) ├── DataGridComponent — Key/value pairs from any ORM instance ├── DetailComponent — Read-only record detail page ├── FormComponent — Full form with validation & delete ├── RowComponent — Bootstrap row of equal-width columns ├── StatsCardComponent — Single KPI metric card └── TabulatorComponent — Interactive Tabulator.js data table

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.

title: Mapped[str] = mapped_column( String(255), info={ "label": "Project Title", "ui": "text", # widget: text | textarea | select | checkbox | file … "visible": True, # show column in Tabulator list view "size": 6, # Bootstrap col-md-6 "placeholder": "My Project", } )

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

demo
Inventory & ERP

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.

portfolio
Headless Portfolio CMS

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.

Live Development Mode — Un override docker-compose monta ./core come bind-mount e abilita Uvicorn --reload, permettendo il ricaricamento istantaneo del codice del framework all'interno dell'ambiente Docker correttamente isolato.

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)