Nessuna scatola nera. Il sorgente è lì, e si apre.
Costruire parlando non vuol dire non poter guardare. Ogni file del tuo progetto si legge, si corregge e si scarica — da un editor vero dentro il sito — e ogni pezzo ha anche il suo pannello ordinato, per quando la GUI è più veloce del codice.
La modalità avanzata, e da lì c'è tutto
Un interruttore nella sezione Avanzate del progetto: aggiunge la sezione Codice e, in ogni schermata, dice da quale file nasce quello che stai guardando. È spenta di partenza — chi costruisce parlando non ha bisogno di vederla — e la accendi tu, per te.
Più file aperti insieme
Schede che si trascinano e si affiancano, come in un editor da scrivania. Non un campo di testo con dentro un file per volta.
L'albero dei file
Crea, rinomina, sposta, seleziona a gruppi, copia e incolla, carica trascinando dal desktop, scarica un file o una cartella in zip.
Cerca e sostituisci, su tutto
Per percorso e dentro il contenuto, con maiuscole, parola intera ed espressioni regolari. Il risultato si apre dov'è.
Ti dice cosa hai sbagliato
È l'editor vero di Visual Studio Code: completamento, tipi, errori sottolineati mentre scrivi.
Diviso per zone
Tutto il progetto, o solo server/, client/, agents/, repos/, tests/: si sceglie quale albero montare, invece di cercare in una cartella sola.
Le versioni pubblicate in sola lettura
Un menu sceglie la copia da aprire: l'ambiente di prova, che si scrive, o una versione pubblicata, che non cambia più e si legge soltanto. Si scrive sull'ambiente di prova, e si pubblica.
Com'è fatto un progetto, davvero
Sei file di un progetto vero, e accanto a ognuno la cosa che conta di più: quello che non hai dovuto scrivere. Non è che il codice sia poco — è che dice solo la parte che è tua.
{
"description": "Una richiesta aperta da una persona e lavorata dallo staff.",
"scope": "space",
"columns": {
"clienteId": "ref:clienti?",
"titolo": { "type": "text", "minLen": 3, "maxLen": 160 },
"descrizione": { "type": "text?", "maxLen": 4000 },
"stato": "enum:aperta|in_corso|sospesa|chiusa=aperta",
"priorita": "enum:bassa|media|alta=media",
"scadenza": "datetime?"
},
"indexes": ["stato", "createdAt"],
"flow": {
"column": "stato",
"paths": {
"aperta": ["in_corso", "chiusa"],
"in_corso": ["sospesa", "chiusa"],
"sospesa": ["in_corso", "chiusa"],
"chiusa": []
},
"error": "BAD_TRANSITION",
"message": "Una pratica chiusa non si riapre"
},
"access": {
"read": { "rows": "own", "all": ["staff"] },
"create": true,
"update": true,
"delete": true
}
}Da qui nascono la tabella vera sul database, le operazioni di lettura e scrittura, la validazione dei campi, la paginazione, l'ordinamento — e il filtro che fa vedere a chi apre una pratica soltanto le proprie, mentre lo staff le vede tutte (`all`). Chi può leggere lo dice il permesso `pratiche.read` nei ruoli; quali righe, questo file. E il `flow` fa rispettare al database a ogni scrittura che una pratica chiusa non torni aperta. Nessuna di quelle cose è scritta da qualche parte: è dedotta da questo file. Nemmeno `id`, `createdAt`, `updatedAt` e le colonne che dicono a chi appartiene la riga sono qui: le mette la macchina.
import { action, db, fail } from "@spelltech/api-sdk";
export default action({
description: "Apre una nuova pratica a nome di chi chiama",
input: {
titolo: { type: "text", minLen: 3, maxLen: 160 },
descrizione: { type: "text?", maxLen: 4000 },
priorita: "enum:bassa|media|alta?",
clienteId: "ref:clienti?",
},
toast: "Richiesta inviata: la trovi fra le tue pratiche",
async run(data) {
if (data.clienteId) {
const cliente = await db.clienti.findFirst({ where: { id: data.clienteId } });
if (!cliente) fail("CUSTOMER_NOT_FOUND", "Questo cliente non è in anagrafica", { fields: { clienteId: "Scegline uno dall'elenco" } });
}
const pratica = await db.pratiche.create({
data: {
titolo: data.titolo.trim(),
descrizione: data.descrizione,
priorita: data.priorita ?? "media",
clienteId: data.clienteId,
},
});
return { id: pratica.id };
},
});Un file in una cartella e l'operazione esiste, col nome del percorso: `pratiche.apri`, che è anche il nome del suo permesso — chi lo tiene nel suo ruolo la chiama, gli altri no. Niente rotta da registrare, niente controller, niente wiring. I campi in ingresso si dichiarano e arrivano già validati, il database è quello del progetto e di nessun altro, e il messaggio a schermo lo chiede l'azione al frontend. La chiave `trigger`, qui assente, è quella che fa partire lo stesso tipo di file a un orario o alla scrittura di una riga.
import { useState } from "react";
import { definePage, useApiState } from "@spelltech/react";
import { Badge, Card, PageHeader, ToggleGroup, ToggleGroupItem } from "@spelltech/ui";
export default definePage(Pratiche, {
title: "Pratiche",
nav: { label: "Pratiche", icon: "folder-open", order: 20 },
});
const WHERE = {
aperte: { stato: { in: ["aperta", "in_corso", "sospesa"] } },
chiuse: { stato: "chiusa" },
tutte: {},
};
function Pratiche() {
const [filtro, setFiltro] = useState("aperte");
const { data } = useApiState("pratiche.list", {
where: WHERE[filtro], orderBy: { createdAt: "desc" }, take: 200,
});
return (
<>
<PageHeader title="Pratiche" description="Tutto quello che è stato chiesto, con il suo stato." />
<ToggleGroup type="single" value={filtro} onValueChange={(v) => v && setFiltro(v)}>
<ToggleGroupItem value="aperte">Aperte</ToggleGroupItem>
<ToggleGroupItem value="chiuse">Chiuse</ToggleGroupItem>
<ToggleGroupItem value="tutte">Tutte</ToggleGroupItem>
</ToggleGroup>
{data?.items.map((p) => (
<Card key={p.id} className="p-4">
<div className="font-medium">{p.titolo}</div>
<Badge>{p.stato}</Badge>
</Card>
))}
</>
);
}Il file è la rotta e la voce di menu. Non c'è un colore scritto: i componenti leggono le variabili CSS del progetto. E `useApiState` non è una fetch — tiene il dato in cache, lo rilegge quando il server dice che è cambiato e aggiorna la lista sotto gli occhi di chi la sta guardando.
import { action, db, mail, users } from "@spelltech/api-sdk";
export default action({
description: "Manda il promemoria il giorno prima",
trigger: { cron: { for: "prenotazioni", at: "promemoriaAt" } },
async run(prenotazione) {
if (prenotazione.stato !== "confermata" || !prenotazione.ownerUserId) return;
const persona = await users.get(prenotazione.ownerUserId);
if (!persona?.email) return;
const fascia = await db.fasce.findFirst({ where: { id: prenotazione.fasciaId } });
await mail.send({
to: persona.email,
template: "promemoria",
data: { nome: prenotazione.nome, quando: fascia?.inizio ?? "" },
});
},
});L'orario sta nel file, non in una schermata di configurazione da tenere allineata: qui è il campo `promemoriaAt` di ogni prenotazione, e la piattaforma sveglia il file una volta per riga quando quell'istante arriva. Gira una volta sola anche con più copie del runtime in piedi, e le email partono col tuo aspetto e dal tuo mittente perché il progetto ha già un tema per le email. Una scrittura, un'email, un pagamento, un webhook, una coda: sono la stessa chiave `trigger`. Una coda dichiara anche quanti tentativi fare (`maxAttempts`) e con che attesa fra l'uno e l'altro (`backoff`); un errore dell'infrastruttura rimette il lavoro in coda senza consumarne uno.
{
"timezone": "Europe/Rome",
"locale": "it",
"roles": {
"owner": { "label": "Titolare", "can": ["*"] },
"staff": {
"label": "Staff",
"description": "Lavora le pratiche di tutti e tiene l'anagrafica.",
"can": ["pratiche.*", "clienti.*", "note.*"]
},
"cliente": {
"label": "Cliente",
"description": "Apre le proprie richieste e ne segue l'avanzamento.",
"default": true,
"can": ["pratiche.read", "pratiche.apri", "note.read", "note.aggiungi"]
}
}
}Un permesso non si dichiara: `pratiche.apri` esiste perché esiste l'azione che porta quel nome, `pratiche.read` perché la tabella ha la lettura, quindi non c'è un elenco da tenere allineato né un nome che si possa scrivere storto. Qui si scrive una volta sola chi lo tiene, e vale in ogni schermata, in ogni azione e in ogni chiamata con una chiave API — niente `if` sul ruolo sparso per il codice. Il permesso dice SE; quali RIGHE, lo dice la tabella.
{
"region": "fra",
"size": { "cpuKind": "shared", "cpus": 2, "memoryMb": 2048 },
"diskGb": 10,
"packages": { "base": "debian:bookworm-slim", "apt": ["ffmpeg", "nodejs"] },
"cmd": ["node", "server.mjs"],
"ports": [
{ "port": 8080, "protocol": "http", "access": "public" }
],
"secrets": ["STRIPE_KEY"],
"sleep": { "enabled": true, "afterMin": 3 },
"roles": ["render"],
"envs": { "prod": { "size": { "cpus": 4, "memoryMb": 4096 }, "diskGb": 20 } }
}Una macchina è una cartella del progetto: il manifesto e accanto il codice che ci gira dentro. Le porte dichiarate sono anche il firewall — quello che non è scritto qui non si raggiunge — e la macchina si addormenta da sé quando nessuno le parla, quindi il silenzio non si paga. Ce n'è una per ambiente: `envs` dice cosa cambia in produzione (taglia, disco, sonno), il resto è uguale ovunque.
Un pannello ordinato per ogni pezzo
Un editor è la risposta giusta a metà delle domande. Per l'altra metà — guardare mille righe, capire chi può chiamare un'azione, vedere quanto occupano i file — serve una schermata fatta per quello, e c'è.
Database
Le tabelle e le viste in schede, come in un client vero: griglia paginata e ordinabile, modifica di un valore col doppio click, aggiunta e cancellazione di righe, e una scheda per le query SQL scritte a mano.
Il disegno delle relazioni
Il diagramma di come le tabelle si legano, disposto da sé: tabelle in blu, viste in verde, le chiavi esterne come archi. Si trascina, si ingrandisce, e il doppio click su una tabella la apre nei dati.
API
L'elenco di tutte le unità del progetto, ognuna con ciò che la fa partire — una chiamata, un orario, una tabella, un'email, un pagamento, un webhook, una coda, un canale. Di una chiamata vedi la riga pronta da copiare, i campi che accetta e chi può farla; di tutte le altre il tasto «Esegui adesso» e le ultime esecuzioni. In schede: i test, e i canali con chi c'è dentro ora.
Documenti
Due specie in due schede: i file pubblici, serviti a chiunque dal tuo dominio, e quelli privati, a cui si arriva solo passando dall'applicazione. Con quanto occupano, quanto pesano davvero dopo la deduplicazione, e chi li nomina nel codice.
Segreti e chiavi
I segreti che il codice legge, nella cassaforte del progetto: prova e produzione affiancati, valori nascosti e da rivelare uno alla volta dopo una riconferma d'identità. Accanto, i valori non segreti che una pagina legge. E le chiavi API con cui un altro sistema legge e scrive i tuoi dati: ruoli, limiti al secondo, scadenza, revoca.
Le email del progetto con l'anteprima resa esattamente come partirà, l'editor del corpo accanto, le variabili rilevate coi valori di prova, e l'invio a un indirizzo per vederla arrivare davvero.
Quando si scrive codice, e quando no
La regola dello stack è una sola: quello che si può dichiarare si dichiara, quello che è logica vera si scrive. Serve a te come serve all'agente — meno modi di sbagliare, e le cose giuste sono anche le più brevi.
- Il progettoIl sorgentework/
- Dove vive ogni cosaLe tabelle, dichiaratedata/La logica che è tuaserver/Pagine e aspettoclient/I ruoli di partenzaproject.json
- Cosa ci pensa la piattaformaOperazioni e validazionenascono dalle tabellePermessi e tempo realeosservati, non scritti
Non lo scrivi per le cose che si dichiarano
Una tabella è un file di dati e da lì nascono le operazioni, la validazione, la paginazione e il filtro per proprietario. Anche le regole che di solito si scrivono a mano si dichiarano sulla tabella: due prenotazioni che non si sovrappongono, un massimo di righe per persona, un conto a crediti e debiti, le fasce orarie, e le transizioni di stato ammesse — una pratica chiusa non torna aperta — e le fa rispettare il database a ogni scrittura. I ruoli sono dati, e i permessi sono i nomi delle operazioni. L'aspetto è un foglio di stile con le variabili CSS. Non c'è codice da scrivere per queste cose, quindi non c'è codice da mantenere.
Lo scrivi per la logica che è tua
Il calcolo del totale, la regola che decide se una prenotazione si può accettare, il conto del mese: quello è il tuo mestiere e si scrive in un file. TypeScript o JavaScript, come preferisci: i tipi non si compilano, si controllano — e i tipi del tuo database, delle tue azioni e delle tue rotte li scrive la piattaforma, così un nome sbagliato si vede prima di provarlo.
E lo eietti quando la parte generata non basta
Un'operazione generata si sostituisce con la tua — un file col nome dell'operazione, accanto alla tabella — una per volta, senza rinunciare alle altre. Un componente del kit si rimpiazza mettendo il tuo file nel progetto. Nessuna esportazione, nessun punto di non ritorno.
Cosa si controlla prima di dire «fatto»
Il progetto attinge a un deposito di librerie condiviso, con una libreria sola per ogni lavoro — pdfmake per un PDF nuovo, exceljs per un foglio Excel, bwip-js per un codice a barre —: quello che serve c'è già installato, e la schermata Librerie dice cosa può entrare nel backend e cosa nelle pagine. Le chiavi dei servizi esterni non stanno nel codice: stanno nella cassaforte del progetto, e il codice le legge da lì.
La verifica finale
Quando l'agente finisce, fa girare la verifica finale: ogni scenario, i controlli derivati dalle dichiarazioni, il disegno di ogni pagina per ogni persona che la apre e i tipi del frontend e del backend, senza nessun modello. Un errore di tipo è un fallimento; un controllo che non ha potuto guardare niente è segnalato, non conta come verde. L'agente legge i fallimenti e li corregge prima di rispondere.
La copertura, scritta
Gli scenari sono dati, non codice: chi fa cosa e cosa deve risultare. La copertura dice cosa ne ha uno — per ogni azione il percorso che riesce e ogni rifiuto che può dare, per ogni pagina ogni persona che la apre, per ogni email la sua composizione — e cosa ancora no.
Il codice gira in una gabbia
Le tue unità girano isolate dal resto: senza privilegi di sistema, senza file né processi, con la rete aperta solo verso l'esterno e non verso i servizi interni della piattaforma. Le pagine che il server scrive in anticipo non vedono il database né le credenziali, e le loro richieste in uscita non raggiungono la rete interna.
L'agente prova davvero
Esegue l'azione come un utente vero, apre la pagina in un browser e percorre il flusso, e i controlli che contano si salvano come scenari del progetto, da rieseguire su un ambiente di test che si disfa alla fine. Un errore trovato subito costa un messaggio, uno trovato dopo costa una sera.
I documenti restano di chi li ha
Una riga può nominare un documento privato solo se chi la scrive l'ha appena caricato o lo vede già in una riga sua. Nessuno fa puntare un proprio record al file di un altro per scaricarlo: la regola sta nel punto da cui passano tutte le scritture, generate o tue.
Ogni modifica ha un ritorno
Si torna al punto di prima, e le copie programmate riportano l'ambiente com'era — anche solo il pezzo che si è rotto.
Quello che ti porti
Un lock-in non si smentisce a parole: si smentisce dicendo cosa esce. Ecco cosa esce.
Il codice si scarica
Un file, una cartella o l'albero in zip, dalla stessa schermata in cui lo modifichi.
I dati sono tuoi
Il database del progetto è tuo e separato da tutti gli altri; le righe si leggono, si interrogano in SQL e si portano via.
E può stare sul tuo GitHub
La piattaforma collega il GitHub della tua organizzazione e mette lì il codice nativo: il repository è tuo, con la tua cronologia.
Quello che non si porta via è la piattaforma: le operazioni generate, i permessi osservati, il tempo reale e la pubblicazione sono cose che fa lei. È il motivo per cui il tuo codice è così corto.
Il modo più veloce di controllarlo è aprirlo.
Fai un progetto, accendi la modalità avanzata e leggi tutto quello che l'agente ha scritto. La dotazione iniziale basta.













