Tasks
Informazioni · v1.0

Come è fatta ELVETH Tasks

Dashboard di automazione che trasforma il flusso "una richiesta a Claude Code alla volta" in una coda asincrona di task eseguiti su VPS, con PR su GitHub pronte da revisionare. Qui sotto trovi l'infrastruttura in dettaglio: Cloudflare Worker unificato, schema D1, VPS Hostinger, runner Node + Claude Code Max.

Worker Cloudflare
1
Static + API + Queue
VPS Frankfurt
~5€/mese
Hostinger KVM 1
Tabelle D1
3
projects · tasks · workers
Architettura

Architettura ad alto livello

Un solo Worker fa tre cose contemporaneamente: serve la dashboard statica, espone l'API privata per il pannello, espone l'API pubblica /api/queue/* per il VPS. Cloudflare Access protegge tutto tranne la queue, che usa Bearer token.

Cloudflare Worker · tasks.elveth.com
Static Assets
Next.js export
  • tasks.elveth.com/
  • tasks.elveth.com/projects
  • tasks.elveth.com/tasks
  • tasks.elveth.com/settings
Hono handler
API e queue
  • /api/projects · CRUD
  • /api/tasks · CRUD
  • /api/stats · counters
  • /api/queue/* · Bearer
  • /webhooks/github
↓ bindings
D1 (SQLite distribuito)
binding: DB
  • projects
  • tasks
  • workers_heartbeat
R2 (object storage)
binding: LOGS
  • output log
  • diff archive
  • artefatti
↑ GET /api/queue/next ogni 10s — Bearer auth
VPS Hostinger · Frankfurt
Daemon Node
PM2
  • claim → run → push
Claude Code Max
OAuth piano Max
  • eseguito in cwd repo
git + gh
branch claude/<id>
  • push + apri PR
↓ git push · gh pr create
github.com/elveth · repo dei progetti
PR di review → merge manuale → deploy automatico dei progetti collegati
Worker unificato

Dashboard Next.js (output: 'export') e API Hono nello stesso Worker. Same-origin: niente CORS, un solo cookie di sessione.

Access + Bearer

L'UI è dietro Cloudflare Access (solo email Elveth). /api/queue/* è in path exclusion e si autentica via VPS_API_TOKEN.

Polling outbound

Il VPS sta dietro NAT: nessuna porta aperta. Chiede al Worker `GET /api/queue/next` ogni 10s ed esegue se trova lavoro.

Cloudflare

Lo strato Cloudflare

Tutto quello che gira in cloud sta su Cloudflare: un solo Worker con Static Assets + Hono, D1 come database, R2 per i log, Access come gateway di autenticazione.

Workers — runtime serverless
Nome progetto
elveth-tasks
Dominio
tasks.elveth.com
Entry
apps/worker/src/index.ts
Compatibility flags
nodejs_compat
Static assets
./apps/panel/out (Next export)
Deploy
pnpm deploy (build:panel + wrangler deploy)
D1 — SQLite distribuito
Binding
DB
Database name
elveth-tasks-db
ORM
Drizzle
Migrations
./packages/db/migrations
Migration corrente
0001_init.sql
Tabelle
projects · tasks · workers_heartbeat
R2 — object storage
Binding
LOGS
Bucket
elveth-tasks-logs
Uso
Log completo output Claude + artefatti per task
Upload
Presigned URL via /api/queue/log-upload-url
Chiave
tasks/<task_id>/output.log
Access — gateway autenticazione
Provider
Cloudflare Zero Trust
Politica
Email @elveth.com
Sessione
Cookie (same-origin con API)
Path exclusion
/api/queue/*
Auth queue
Bearer VPS_API_TOKEN (32 byte hex)
Path exclusion: la regola critica

Il VPS chiama /api/queue/* senza login utente. Quei path devono essere esclusi da Access (altrimenti riceverebbero il redirect alla pagina di login). La verifica si fa con curl -i https://tasks.elveth.com/api/queue/next?worker=test: deve rispondere 401 con messaggio del Worker, non un redirect HTML.

Endpoint REST
Privati (Cloudflare Access)
  • GET/api/projects
  • POST/api/projects
  • GET/api/projects/:id
  • PATCH/api/projects/:id
  • DELETE/api/projects/:id
  • GET/api/tasks
  • POST/api/tasks
  • GET/api/tasks/:id
  • PATCH/api/tasks/:id
  • DELETE/api/tasks/:id
  • GET/api/stats
  • GET/api/stats/:project_id
VPS (Bearer)
  • GET/api/queue/next?worker=<id>
  • GET/api/queue/projects/:id
  • PATCH/api/queue/tasks/:id
  • POST/api/queue/heartbeat
  • POST/api/queue/log-upload-url
  • POST/api/webhooks/github
VPS Hostinger

VPS Hostinger — il braccio esecutivo

L'unico componente che chiama gli altri: sta dietro NAT, niente porte aperte. Pollinga il Worker e quando trova un task lo esegue con Claude Code Max, poi apre PR su GitHub.

Hardware & OS
Provider
Hostinger
Piano
KVM 1
CPU
1 vCPU
RAM
4 GB
Storage
50 GB SSD
Location
Frankfurt (DE)
Costo
~5€/mese
OS
Ubuntu 24.04 LTS
Software installato
Runtime
Node.js 20 LTS
Process manager
PM2 (restart automatico)
Claude CLI
@anthropic-ai/claude-code (piano Max)
Git
CLI standard
GitHub CLI
gh (token PAT scope repo)
Sync logs
rclone
Worker per VPS
1 (parallelismo arriva in v2)
Autenticazioni del VPS
Claude Code Max
OAuth (browser)

Login una sola volta col piano Max via SSH tunnel; sessione persistente sul disco.

GitHub
PAT scope repo

Token salvato in ambiente del daemon; permette push e gh pr create sull'org elveth.

Worker queue
Bearer token

Variabile VPS_API_TOKEN identica al secret sul Worker (openssl rand -hex 32).

Runner & Claude

Il runner & il loop di Claude

Il daemon Node sul VPS è uno script semplice che fa una sola cosa: ciclicamente chiede un task, lo esegue, riporta l'esito. Repo separato (elveth/tasks-runner).

Loop del daemon (ogni 10s)
  1. 1
    claim_next

    GET /api/queue/next?worker=<id> — atomic UPDATE che marca un pending come running e lo restituisce. Niente race condition.

  2. 2
    sync repo

    cd su working_dir del progetto, git fetch + checkout main + pull, poi nuovo branch claude/task-<id>.

  3. 3
    claude -p

    Esegue Claude Code Max passando base_prompt del progetto concatenato al prompt del task. Stesso ambiente di Mattia in locale.

  4. 4
    commit + push

    Stage delle modifiche, commit con messaggio generato dal log Claude, git push -u origin del branch.

  5. 5
    gh pr create

    Apre PR con titolo dal task e body con sintesi dell’output. Mai automerge.

  6. 6
    PATCH tasks/:id

    Riporta esito (done / failed / needs_review), pr_url, output_summary e la chiave R2 del log completo.

Pattern needs_review
Trigger
Output Claude contiene [NEEDS_REVIEW: ...]
Stato task
needs_review (auto-resume su pending all’answer)
Campo question
Estratto dal marker
Risposta utente
PATCH /api/tasks/:id { answer }
Effetto
Status torna pending, il task rientra in coda
Branch e PR
Naming branch
claude/task-<id>
Base branch
main del progetto
Push
git push -u origin <branch>
PR
gh pr create (mai automerge)
Review
Mattia approva e mergea via GitHub web/mobile
Heartbeat

Ogni 30 secondi il worker chiama POST /api/queue/heartbeat con il proprio ID, lo stato corrente (idle / working / error) e il task in lavorazione. La tabella workers_heartbeat permette di sapere dalla dashboard se il VPS è vivo e cosa sta facendo.

Schema DB

Schema D1 — tre tabelle, niente di più

Lo schema vive in packages/db (Drizzle ORM + SQL backup) e gira su Cloudflare D1. Single source of truth: packages/shared/src/entities.ts.

projects· 13 colonne

Contenitore di task. Mappato 1:1 a un repo GitHub e a una working directory sul VPS.

CampoTipoNote
idTEXT PKproj_<random>
nameTEXTNome leggibile, mostrato in UI
slugTEXT UNIQUEkebab-case, usato in /projects/[slug]
descriptionTEXTSintesi breve
github_repoTEXTFormato org/name (es. elveth/tasks)
default_branchTEXTDefault "main"
base_promptTEXTContesto fisso prepended a ogni task
project_typeTEXTshopify · cloudflare · generic
working_dirTEXTPath su VPS dove vive il clone
activeINTEGER1 attivo · 0 archiviato (soft delete)
colorTEXTHex per la UI
created_atINTEGERunix seconds
updated_atINTEGERunix seconds
tasks· 21 colonne

Unità di lavoro per Claude. Un task = un branch = una PR. Si muove tra gli stati di TaskStatus.

CampoTipoNote
idTEXT PKtask_<random>
project_idTEXT FK→ projects(id) ON DELETE CASCADE
titleTEXT≤100 char, mostrato in lista
promptTEXTDescrizione dettagliata per Claude
statusTEXTpending · running · done · failed · needs_review · merged
priorityINTEGER-1 bassa · 0 normale · 1 alta
branchTEXTclaude/task-<id> (popolato dal worker)
pr_urlTEXTURL della PR su GitHub
pr_numberINTEGERNumero PR
preview_urlTEXTURL preview (Cloudflare / Shopify)
output_summaryTEXTSintesi mostrata in UI
output_log_keyTEXTChiave R2 del log completo
questionTEXTSe status=needs_review: la domanda di Claude
answerTEXTRisposta utente; trigger auto-resume a pending
worker_idTEXTQuale worker VPS ha preso il task
cost_usdREALCosto (per futuri piani a consumo)
duration_secondsINTEGERDurata esecuzione
error_messageTEXTCompilato se status=failed
created_atINTEGERunix seconds
started_atINTEGERunix seconds
completed_atINTEGERunix seconds
workers_heartbeat· 5 colonne

Stato live dei worker VPS. In v1 ce n'è uno solo; il parallelismo arriva in v2.

CampoTipoNote
worker_idTEXT PKes. vps-1-w1
last_seenINTEGERunix seconds — aggiornato ogni 30s
current_task_idTEXT FK→ tasks(id) ON DELETE SET NULL
statusTEXTidle · working · error
metadataTEXTJSON: hostname, versione runner, ecc.
GitHub

GitHub e flusso PR

Il codice vive su github.com/elveth. Il runner apre PR; Mattia approva sempre manualmente. main è protetto: niente push diretti, niente automerge.

Branch protection
Branch protetto
main (default branch)
Block force push
on
Restrict deletions
on
Require PR before merging
on (0 approvals richieste)
Naming branch automatici
claude/task-<id>
Webhook & deploy
Webhook GitHub
POST /api/webhooks/github
Eventi ascoltati
pull_request (opened, closed, merged)
Sincronizzazione
Aggiorna status del task in DB
Deploy progetto
Su merge → CI del progetto stesso
ELVETH Tasks
Cloudflare Workers Build su push a main
Stack tecnico

Stack tecnico

Niente magie: tech che usiamo già altrove (Cloudflare, GitHub, Hostinger), strumenti veloci e poche dipendenze.

LayerTecnologiaNote
FrontendNext.js 15 · React 19 · TypeScriptoutput: 'export' → assets statici servits dal Worker
UITailwind CSS v4 · shadcn/uiDark mode first-class, niente glassmorphism
BackendCloudflare Workers · HonoRouting type-safe, defineTool() per ogni operazione
DatabaseCloudflare D1 · Drizzle ORMSQLite distribuito, migrazioni in packages/db
Storage logCloudflare R2Output completo Claude + diff archive per task
Auth UICloudflare AccessPolitica email @elveth.com, cookie same-origin
Auth queueBearer VPS_API_TOKENSecret del Worker, 32 byte hex
ValidazioneZodInput REST, queue e webhooks; uscita tipizzata
Pkg managerpnpm workspacesMonorepo: apps/* + packages/*
Lint/formatBiomepnpm check deve passare prima del commit
CI/CDCloudflare Workers BuildAuto-deploy su push a main, build + worker in un colpo
VPSHostinger KVM 1 · Ubuntu 24.04 · PM2Node 20 + Claude Code Max + gh CLI
MCP (futuro)@modelcontextprotocol/sdkStesso defineTool() esposto ad agenti AI
Costi

Costi attesi

Quasi tutto è già pagato in altri abbonamenti: l'unico costo aggiuntivo dedicato a ELVETH Tasks è il VPS Hostinger.

Cloudflare
0€incluso piano free

Workers, D1, R2, Access — tutto sotto i limiti gratuiti.

Hostinger KVM 1
~5€al mese

1 vCPU, 4 GB RAM, 50 GB SSD, location Frankfurt.

Claude Max
0€già attivo per Mattia

Login OAuth sul VPS riusa lo stesso piano Max.

GitHub + dominio
0€già pagati

Org elveth su piano esistente, dominio elveth.com già attivo.