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 nelle impostazioni 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 — ed è una proprietà del PROGETTO, non del tuo browser: un progetto può essere aperto di sotto mentre un altro resta un progetto con cui si parla.
Schede che si trascinano e si affiancano, come in un editor da scrivania. Non un campo di testo con dentro un file per volta.
Crea, rinomina, sposta, seleziona a gruppi, copia e incolla, carica trascinando dal desktop, scarica un file o una cartella in zip.
Per percorso e dentro il contenuto, con maiuscole, parola intera ed espressioni regolari. Il risultato si apre dov'è.
È l'editor vero di Visual Studio Code: completamento, tipi, errori sottolineati mentre scrivi.
Frontend, API, agenti, email, e il resto: si sceglie quale albero montare, invece di cercare in una cartella sola.
Puoi guardare cosa è pubblicato, ma non scriverci: la pubblicazione successiva lo sovrascriverebbe. 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.
{
"columns": {
"praticaId": { "type": "id", "primaryKey": true },
"ownerUserId": { "type": "reference", "nullable": false },
"clienteId": { "type": "reference", "references": { "table": "clienti", "column": "clienteId" } },
"titolo": { "type": "text", "nullable": false, "minLen": 3, "maxLen": 160 },
"stato": { "type": "enum", "enum": ["aperta", "in_corso", "sospesa", "chiusa"], "default": "aperta" },
"priorita": { "type": "enum", "enum": ["bassa", "media", "alta"], "default": "media" },
"scadenza": { "type": "datetime", "nullable": true },
"createdAt": { "type": "datetime", "default": "now" }
},
"indexes": [{ "columns": ["ownerUserId"] }, { "columns": ["stato"] }],
"access": {
"ops": {
"list": { "who": ["owner", "role:staff"] },
"get": { "who": ["owner", "role:staff"] },
"update": { "who": ["role:staff"] },
"delete": { "who": ["role:staff"] }
}
}
}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. Nessuna di quelle cose è scritta da qualche parte: è dedotta da questo file.
import { data, db, req, tokenData, fail } from "@spelltech/api-sdk";
import { PRIORITA } from "@lib/pratiche.js";
export const description = "Apre una nuova pratica a nome di chi chiama";
export const access = ["authenticated"];
export const input = {
titolo: { type: "text", required: true, minLen: 3, maxLen: 160 },
descrizione: { type: "text", maxLen: 4000 },
priorita: "enum:bassa|media|alta?",
clienteId: "text?",
};
export default async () => {
if (data.clienteId) {
const cliente = await db.clienti.findFirst({ where: { clienteId: data.clienteId } });
if (!cliente) fail("CLIENTE_INESISTENTE", "Questo cliente non è in anagrafica", { fields: ["clienteId"] });
}
const pratica = await db.pratiche.create({
data: {
ownerUserId: tokenData.userId,
titolo: String(data.titolo).trim(),
priorita: PRIORITA.includes(data.priorita) ? data.priorita : "media",
stato: "aperta",
},
});
req.toast({ type: "success", text: "Richiesta inviata: la trovi fra le tue pratiche" });
return { data: { praticaId: pratica.praticaId } };
};Un file in una cartella e l'azione esiste: niente rotta da registrare, niente controller, niente wiring. L'accesso è una riga, 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.
import { useState } from "react";
import { definePage, useApiState } from "@spelltech/react";
import { Badge, Button, Card, EmptyState, 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, loading } = useApiState("pratiche.list", {
data: { where: WHERE[filtro], sort: "createdAt", order: "desc", limit: 200 },
});
return (
<>
<PageHeader title="Pratiche" description="Tutto quello che è stato chiesto, con il suo stato." />
<ToggleGroup type="single" value={filtro} onValueChange={setFiltro}>
<ToggleGroupItem value="aperte">Aperte</ToggleGroupItem>
<ToggleGroupItem value="chiuse">Chiuse</ToggleGroupItem>
<ToggleGroupItem value="tutte">Tutte</ToggleGroupItem>
</ToggleGroup>
{data?.items.map((p) => (
<Card key={p.praticaId} 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 il tema. 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 { db, mail } from "@spelltech/api-sdk";
export const schedule = "0 * * * *"; // ogni ora
const FINESTRA_ORE = 24;
export default async function () {
const fino = new Date(Date.now() + FINESTRA_ORE * 3_600_000).toISOString();
const fasce = await db.fasce.findMany({
where: { inizio: { gte: new Date().toISOString(), lte: fino } },
take: 500,
});
if (!fasce.length) return;
const prenotazioni = await db.prenotazioni.findMany({
where: { fasciaId: { in: fasce.map((f) => f.fasciaId) }, stato: "confermata" },
});
for (const p of prenotazioni) {
if (p.promemoriaAt) continue; // il timbro impedisce il doppio invio
await mail.send({ to: p.email, template: "promemoria", data: { nome: p.nome } });
await db.prenotazioni.update({
where: { prenotazioneId: p.prenotazioneId },
data: { promemoriaAt: new Date().toISOString() },
});
}
}L'orario sta nel file, non in una schermata di configurazione da tenere allineata. Gira una sola volta per volta 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.
{
"permissions": {
"pratiche.gestisci": { "label": "Prende in carico e chiude le pratiche di chiunque" },
"clienti.gestisci": { "label": "Tiene l'anagrafica dei clienti" },
"persone.gestisci": { "label": "Invita le persone e assegna i ruoli" }
},
"roles": {
"staff": {
"label": "Staff",
"description": "Lavora le pratiche di tutti, tiene l'anagrafica e invita le persone",
"permissions": ["*"]
},
"cliente": {
"label": "Cliente",
"description": "Apre le proprie richieste e ne segue l'avanzamento",
"permissions": [],
"default": true
}
}
}Ruoli e permessi si dichiarano una volta e valgono in ogni schermata, in ogni azione e in ogni chiamata con una chiave API. Non c'è un `if` sul ruolo sparso per il codice: le regole stanno qui, e chi non le rispetta non passa — nemmeno per sbaglio.
{
"region": "fra",
"size": { "cpuKind": "shared", "cpus": 2, "memoryMb": 2048 },
"diskGb": 10,
"image": { "base": "debian:bookworm", "packages": ["ffmpeg", "nodejs"] },
"start": "node server.mjs",
"ports": [
{ "name": "http", "port": 8080, "access": "public" }
],
"secrets": ["STRIPE_KEY"],
"sleep": { "enabled": true, "afterMin": 3 },
"grants": ["db", "fs"]
}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.
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'inventario di cosa il progetto espone e a chi: le azioni raggruppate per cartella, la chiamata pronta da copiare, i campi che accetta e — detto in chiaro — chi può chiamarla. Più i lavori interni, che da fuori nessuno può invocare.
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
Le variabili che il codice legge, coi valori nascosti e da rivelare uno alla volta. 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.
- 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. I permessi sono un file di dati. Il tema è un file di dati. 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 si cancellano, non si compilano.
- E lo eietti quando la parte generata non basta
Un'operazione generata si sostituisce con la tua, 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.
E quello che il codice può importare
Il progetto attinge a un deposito di librerie condiviso: quello che serve c'è già installato, e la schermata Librerie dice esattamente cosa può entrare nel backend e cosa nelle pagine. Le chiavi dei servizi esterni non stanno nel codice: stanno fra i segreti, e il codice le legge da lì.
I tipi devono passare
Il controllo gira sul progetto come girerebbe sul tuo computer, e una pubblicazione con i tipi rotti non parte.
L'agente si rilegge
Dopo aver scritto, controlla: compila, prova l'azione, guarda la schermata. Un errore trovato subito costa un messaggio, uno trovato dopo costa una sera.
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.
Un file, una cartella o l'albero in zip, dalla stessa schermata in cui lo modifichi.
Il database del progetto è tuo e separato da tutti gli altri; le righe si leggono, si interrogano in SQL e si portano via.
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.






