Storage Spaces
DigitalOcean Spaces per asset applicativi e migrazione da Vercel Blob
Scopo
DigitalOcean Spaces e' lo storage oggetti standard per gli asset applicativi che devono uscire da Vercel Blob o da provider esterni durante la migrazione verso il VPS.
Questa non e' una dipendenza obbligatoria per ogni app. Va usata solo dove esiste un bisogno reale di storage file in produzione.
Spazi creati
| Space | Ambiente | Regione | CDN | Origin endpoint |
|---|---|---|---|---|
obelica-assets-prod | produzione | fra1 | abilitato | https://obelica-assets-prod.fra1.digitaloceanspaces.com |
obelica-assets-staging | staging | fra1 | abilitato | https://obelica-assets-staging.fra1.digitaloceanspaces.com |
Endpoint API S3-compatible:
https://fra1.digitaloceanspaces.comScope applicativo
Decisione del 2026-06-22:
| App | Policy storage |
|---|---|
sonja-trekking | target migrazione storage; usa Vercel Blob per upload/delete immagini tour |
tulpa-studio | target migrazione storage; contiene flussi Vercel Blob e Cloudinary, da razionalizzare con cautela |
alessandro-zucchini | migrazione production Cloudinary -> Spaces completata; non era un target Vercel Blob |
wash-dog-ferrara | esclusa dalla migrazione storage per ora; eventuale codice Blob non va considerato produzione reale |
obelica | esclusa dalla migrazione storage per ora |
Le altre app non vanno collegate a Spaces finche' non emerge un requisito reale.
Variabili ambiente
Template produzione:
STORAGE_PROVIDER=spaces
SPACES_REGION=fra1
SPACES_BUCKET=obelica-assets-prod
SPACES_ENDPOINT=https://fra1.digitaloceanspaces.com
SPACES_ORIGIN_URL=https://obelica-assets-prod.fra1.digitaloceanspaces.com
SPACES_PUBLIC_BASE_URL=https://obelica-assets-prod.fra1.cdn.digitaloceanspaces.com
SPACES_ACCESS_KEY_ID=<secret>
SPACES_SECRET_ACCESS_KEY=<secret>Template staging:
STORAGE_PROVIDER=spaces
SPACES_REGION=fra1
SPACES_BUCKET=obelica-assets-staging
SPACES_ENDPOINT=https://fra1.digitaloceanspaces.com
SPACES_ORIGIN_URL=https://obelica-assets-staging.fra1.digitaloceanspaces.com
SPACES_PUBLIC_BASE_URL=https://obelica-assets-staging.fra1.cdn.digitaloceanspaces.com
SPACES_ACCESS_KEY_ID=<secret>
SPACES_SECRET_ACCESS_KEY=<secret>Non committare mai chiavi Spaces o connection string reali.
Prefissi oggetti
Usare prefissi stabili per evitare collisioni e semplificare audit/migrazioni:
<app>/<feature>/<id>/<filename>
sonja-trekking/tours/<tour-id>/<timestamp>.webp
tulpa-studio/cms/media/<media-id>/<filename>
alessandro-zucchini/media/<media-id>/<filename>Migrazione da Vercel Blob
Procedura target:
- Inventariare codice e database della produzione reale.
- Ottenere la sorgente reale Vercel Blob/store o l'elenco URL usato in produzione.
- Copiare gli oggetti su Spaces mantenendo una mappa
old_url -> new_url. - Aggiornare il database con script idempotente e backup pre-migrazione.
- Deployare il provider Spaces nell'app.
- Verificare upload, lettura pubblica, delete e rollback.
- Tenere Vercel Blob in sola lettura finche' la produzione non serve stabilmente gli URL Spaces.
Non eliminare dati da Vercel Blob finche' non esistono:
- backup DB post-migrazione;
- mappa di migrazione conservata;
- smoke test e test manuale delle pagine principali;
- rollback documentato.
Audit del 2026-06-22
Produzione Vercel
Audit read-only eseguito con Vercel CLI autenticata, scaricando temporaneamente gli env produzione fuori repo e cancellandoli a fine comando. Nessun segreto e' stato documentato.
Inventario Vercel Blob:
| App | Blob token produzione | Oggetti Blob | Dimensione totale | Host Blob |
|---|---|---|---|---|
sonja-trekking | presente | 57 | 132.31 MB | pukpqfy6og9fpdh3.public.blob.vercel-storage.com |
tulpa-studio | presente | 267 | 564.01 MB | gs0y25qv70rmbqva.public.blob.vercel-storage.com |
alessandro-zucchini | assente | 0 | 0 MB | n/a |
Riferimenti storage nel database produzione Vercel:
| App | Blob refs | Cloudinary refs | Spaces refs | Colonne principali |
|---|---|---|---|---|
sonja-trekking | 57 | 0 | 0 | public.Tour.imageUrl, public.Tour.additionalImages |
tulpa-studio | 327 refs, 210 URL unici | 0 | 0 | public.media.url, public.product_models.imageUrl, public.projects.coverImageUrl, public.cms_content.content |
alessandro-zucchini | 0 | 28 | 0 | public.Media.url, public.Lesson.videoUrl, public.Lesson.coverImageUrl, public.Portfolio.videoUrls, public.Course.imageUrl, public.Course.previewVideoUrl, public.LessonAttachment.fileUrl, public.Portfolio.thumbnailUrl |
Conclusione operativa:
sonja-trekkinge' il primo candidato ideale per migrazione Vercel Blob -> Spaces: pochi riferimenti e tutti gli URL DB sono presenti nel listing Blob;tulpa-studioe' il secondo candidato, piu' ampio e con piu' feature media;alessandro-zucchininon era una migrazione Vercel Blob: e' stata completata come migrazione Cloudinary -> Spaces il 2026-06-23.
Pilota Sonja Trekking
Decisione operativa del 2026-06-23:
- Sonja e' il progetto pilota per il passaggio Vercel Blob -> DigitalOcean Spaces.
- Il codice deve mantenere
vercelcome provider default finche' la migrazione non e' pronta. - Lo switch produzione finale deve avvenire cambiando solo
STORAGE_PROVIDERdavercelaspaces.
Env produzione Vercel gia' preparate:
STORAGE_PROVIDER=spaces
SPACES_REGION=fra1
SPACES_BUCKET=obelica-assets-prod
SPACES_ENDPOINT=https://fra1.digitaloceanspaces.com
SPACES_ORIGIN_URL=https://obelica-assets-prod.fra1.digitaloceanspaces.com
SPACES_PUBLIC_BASE_URL=https://obelica-assets-prod.fra1.cdn.digitaloceanspaces.comVariabili sensitive impostate in Vercel Production:
SPACES_ACCESS_KEY_ID=<secret>
SPACES_SECRET_ACCESS_KEY=<secret>Stato del 2026-06-23:
- credenziali Spaces production reinserite come env sensitive Vercel;
- test credenziali eseguito con put -> HEAD CDN 200 -> delete su oggetto temporaneo;
- copia Vercel Blob -> Spaces completata per gli oggetti disponibili;
- backup DB production immediatamente prima del rewrite:
.storage-migration/sonja-production-before-spaces-repair-20260622T231345Z.dump; - rewrite DB production completato usando
DATABASE_URL_UNPOOLED; STORAGE_PROVIDER=spacesattivo in Vercel Production;- deployment production Vercel successivo allo switch:
sonja-trekking-nkuadjxgo-bona-pro.vercel.app, statoReady; - smoke pubblico su
https://www.sonjatrekking.com/it: HTTP 200.
Verifica DB post-migrazione:
imageBlob=0
imageSpaces=21
additionalBlobUrls=0
additionalSpacesUrls=26
toursWithAdditionalBlob=0
toursWithAdditionalSpaces=9
spacesUrlsChecked=46
spacesUrlsFailed=0Note sui riferimenti rotti:
- durante il dry-run corretto sono emersi 4 URL Blob presenti nel DB ma non nel listing Vercel Blob;
- tutti e 4 rispondevano HTTP 404;
- 3 immagini aggiuntive 404 del tour
San Vigilio, tipico ladinosono state rimosse dall'arrayadditionalImages; - l'immagine principale 404 del tour
Fanes e il fascino delle sue acquee' stata sostituita con la prima immagine aggiuntiva valida dello stesso tour; - nessun URL
blob.vercel-storage.comrimane nel DB production.
Il codice Sonja introduce:
- provider storage
vercel/spaces; - upload immagini via
STORAGE_PROVIDER; - delete compatibile con URL Vercel Blob e URL Spaces;
- prefisso oggetti Spaces
sonja-trekking/; - supporto Next Image per host
digitaloceanspaces.com; - script migrazione dry-run/copy/apply-db.
Comandi script:
npm run storage:dry-run
npm run storage:copy
npm run storage:apply-db
npm run storage:apply-db:repairDry-run produzione del 2026-06-23:
blobObjects=57
referencedBlobUrls=50
mappedReferencedUrls=46
missingReferencedUrls=4
repairMissingReferences=enabledSequenza di migrazione Sonja, completata:
- Aggiungere le due env sensitive Spaces in Vercel Production.
- Deployare il codice storage nuovo lasciando
STORAGE_PROVIDER=vercel. - Eseguire
storage:dry-runcon env produzione e verificare la sorgente DB corretta. - Eseguire
storage:copyper copiare tutti gli oggetti Blob in Spaces e salvare la mappa ignorata da git. - Verificare a campione URL Spaces/CDN.
- Fare backup DB produzione.
- Eseguire
storage:apply-db:repairusando la mappa, per gestire i riferimenti 404 gia' rotti. - Verificare sito e admin con
STORAGE_PROVIDER=vercel: le immagini devono gia' puntare a Spaces. - Cambiare
STORAGE_PROVIDER=spacesin Vercel Production. - Testare upload nuovo, visualizzazione e delete.
Tulpa Studio
Stato operativo del 2026-06-23: migrazione production da Vercel Blob a DigitalOcean Spaces completata.
Risultato finale:
- Vercel Production
tulpa-studiousaSTORAGE_PROVIDER=spaces; - bucket target
obelica-assets-prod, regionefra1, CDN DigitalOcean abilitato; - prefisso oggetti Spaces:
tulpa-studio/; - CORS bucket configurato per
https://tulpastudio.comehttps://www.tulpastudio.com; - deploy production Vercel completato e aliasato a
https://www.tulpastudio.com; - DB produzione riscritto da URL Vercel Blob a URL CDN DigitalOcean Spaces;
- Vercel Blob resta da tenere in sola lettura come rollback storico finche' la produzione non e' stabile.
Audit pre-migrazione produzione:
blobObjects=267
blobBytes=591405916
referencedBlobUrls=210
dbReferences=327
missingReferencedUrls=0
orphanedBlobObjects=57
orphanedBlobBytes=208206825Copia oggetti:
npm run storage:copy -- --env-file=<env>
copy=done
objectsCopied=267/267
mapPath=.storage-migration/tulpa-vercel-blob-to-spaces-map.jsonVerifica copia:
map entries=267
HEAD CDN sample=60/60 OKBackup DB creato immediatamente prima del rewrite:
.storage-migration/tulpa-production-before-spaces-20260623T003824Z.dumpRewrite DB produzione:
npm run storage:apply-db -- --env-file=<env>
mode=write-capable
databaseUrlSource=DATABASE_URL_UNPOOLED
referencedBlobUrls=210
dbReferences=327
mappedReferencedUrls=210
missingReferencedUrls=0
updatedRows=254
dbRewrite=doneAudit post-migrazione DB produzione:
uniqueBlobUrls=0
uniqueSpacesUrls=210
spacesHost=obelica-assets-prod.fra1.cdn.digitaloceanspaces.com
HEAD all Spaces URLs=210/210 OKDistribuzione riferimenti URL dopo rewrite:
| Campo | Riferimenti URL estratti |
|---|---|
media.url | 134 |
product_models.imageUrl | 104 |
projects.coverImageUrl | 13 |
projects.vimeoUrl | 13 |
site_settings.value | 2 |
cms_content.content | 94 |
Smoke pubblico post-migrazione:
| URL | Esito |
|---|---|
https://www.tulpastudio.com/ | HTTP 200 |
https://www.tulpastudio.com/portfolio | HTTP 200 |
https://www.tulpastudio.com/rental | HTTP 200 |
Codice storage attivo:
- provider storage
vercel/spaces, con default conservativovercel; - endpoint
POST /api/cms/media/upload-targetper presigned upload Spaces; - upload admin compatibile con Vercel Blob o Spaces;
- delete/move compatibili con URL Vercel Blob e URL Spaces;
- supporto Next Image per
digitaloceanspaces.com; - script migrazione dry-run/copy/apply-db:
scripts/migrate-vercel-blob-to-spaces.ts.
Verifica manuale ancora consigliata:
- Login admin Tulpa.
- Upload nuovo media in media library.
- Inserimento media in pagina/prodotto/progetto.
- Visualizzazione pubblica.
- Delete media test.
Alessandro Zucchini
Stato operativo del 2026-06-23: migrazione production da Cloudinary a DigitalOcean Spaces completata.
Risultato finale:
- Vercel Production
alessandro-zucchiniusaSTORAGE_PROVIDER=spaces; - bucket target
obelica-assets-prod, regionefra1, CDN DigitalOcean abilitato; - prefisso oggetti Spaces:
alessandro-zucchini/; - CORS bucket configurato anche per
https://www.alessandrozucchini.com,https://alessandrozucchini.come varianti storichealessandro-zucchini.com; - DB produzione senza URL Cloudinary residui;
- 17 URL Spaces unici verificati con HEAD HTTP 200;
- Cloudinary resta supportato nel codice come provider fallback, ma non e' piu' la sorgente storage della produzione.
Deploy produzione:
deploy preparatorio storage adapter: dpl_3tMVDhfeLXt9XHcS4jAWuwYo6W9B
deploy switch STORAGE_PROVIDER=spaces: dpl_9AtPTtvqWMH4QibSswiWeLcW24fg
alias produzione: https://www.alessandrozucchini.comAudit pre-migrazione produzione:
dbReferences=28
uniqueCloudinaryUrls=18
validCloudinaryUrls=17
missingCloudinaryUrls=1
copyBytesKnown=95601798Il riferimento mancante era una cover lezione Cloudinary gia' HTTP 404 in produzione, presente in due lezioni di test. La migrazione l'ha riparato impostando Lesson.coverImageUrl a null per quelle due righe.
Copia oggetti:
npm run storage:copy -- --env-file=<env> --repair-missing
copy=done
objectsCopied=17/17
mapPath=.storage-migration/alessandro-cloudinary-to-spaces-map.json
HEAD CDN all copied URLs=17/17 OKBackup DB creato immediatamente prima del rewrite:
.storage-migration/alessandro-production-before-spaces-20260623T080219Z.dumpRewrite DB produzione:
npm run storage:apply-db -- --env-file=<env> --repair-missing
mode=write-capable
databaseUrlSource=DATABASE_URL_UNPOOLED
referencedCloudinaryUrls=18
dbReferences=28
mappedReferencedUrls=17
missingReferencedUrls=1
updatedRows=24
repairedRows=2
dbRewrite=doneAudit post-migrazione DB produzione:
totalUrlOccurrences=26
uniqueCloudinaryUrls=0
uniqueSpacesUrls=17
spacesHost=obelica-assets-prod.fra1.cdn.digitaloceanspaces.com
HEAD all Spaces URLs=17/17 OKDistribuzione riferimenti URL dopo rewrite:
| Campo | Riferimenti URL estratti |
|---|---|
Media.url | 16 |
Lesson.videoUrl | 4 |
LessonAttachment.fileUrl | 1 |
Course.imageUrl | 1 |
Course.previewVideoUrl | 1 |
Portfolio.thumbnailUrl | 1 |
Portfolio.videoUrls | 2 |
Smoke pubblico post-migrazione:
| URL | Esito |
|---|---|
https://www.alessandrozucchini.com/it | HTTP 200 |
https://www.alessandrozucchini.com/it/corsi | HTTP 200 |
https://www.alessandrozucchini.com/it/portfolio/carnevale-di-venezia | HTTP 200 |
Codice storage attivo:
- provider storage
cloudinary/spaces, con default conservativocloudinary; - endpoint
POST /api/admin/storage-upload-targetper presigned upload Spaces o signed Cloudinary upload; - media library admin compatibile con Cloudinary o Spaces;
- upload server-side e lesson attachments compatibili con Cloudinary o Spaces;
- delete compatibile con URL Cloudinary e URL Spaces;
- supporto Next Image per
digitaloceanspaces.com; - script migrazione dry-run/copy/apply-db:
scripts/migrate-cloudinary-to-spaces.ts.
Verifica manuale ancora consigliata:
- Login admin Alessandro.
- Upload nuovo media in media library.
- Upload/delete lesson attachment di test.
- Visualizzazione pubblica del media nuovo.
- Delete media test.
Database DigitalOcean attuale
Nel managed database DigitalOcean attualmente collegato alle app non sono stati trovati URL contenenti:
blob.vercel-storage.com;res.cloudinary.comocloudinary;digitaloceanspaces.com.
Risultato per DB controllati:
| App | Match storage nel DB DO attuale |
|---|---|
alessandro-zucchini | 0 |
sonja-trekking | 0 |
tulpa-studio | 0 |
wash-dog-ferrara | 0 |
Questo non prova che la produzione Vercel non abbia asset. Significa solo che il database DigitalOcean oggi non contiene riferimenti storage noti. Prima di migrare dati reali bisogna identificare la sorgente effettiva usata dalla produzione Vercel.
CORS
Il CORS serve solo per upload diretti dal browser verso Spaces. Upload server-side tramite API applicativa non richiede CORS per la scrittura.
Policy consigliata:
- abilitare CORS solo sulle app che fanno upload diretto browser;
- usare origin espliciti, non wildcard, per produzione;
- includere gli origin staging
*.vps.obelica.comsolo per test; - fare purge CDN dopo modifiche CORS se il comportamento non cambia subito.
Sicurezza chiavi
La prima chiave creata per test non deve diventare la chiave permanente di produzione se e' troppo ampia.
Prima della migrazione produzione:
- creare chiavi granulari per bucket/ambiente quando possibile;
- separare chiavi staging e produzione;
- ruotare qualunque chiave condivisa in chat o screenshot;
- salvare segreti solo nei secret store/env locali previsti;
- non documentare mai valori segreti in Fumadocs.