Salta ai contenuti

Database e Bucket Multipli

Un progetto non è limitato a un solo database e un solo bucket. Le collezioni instradano già tramite dataSource, e le proprietà dei file instradano tramite storageSource; questa pagina spiega come ogni origine con nome ottiene la propria configurazione.

Due passaggi: dichiara le origini nel tuo pacchetto di configurazione, quindi configura ciascuna di esse con variabili d’ambiente derivate dalla sua chiave.

Esporta dataSources e storageSources dall’file index.ts del tuo pacchetto di configurazione. Sono condivisi con il frontend, che utilizza le stesse dichiarazioni per decidere se comunicare con un’origine tramite l’API di Rebase o direttamente.

// config/index.ts
import type { DataSourceDefinition, StorageSourceDefinition } from "@rebasepro/types";
export const dataSources: DataSourceDefinition[] = [
{ key: "(default)", engine: "postgres" },
{ key: "analytics", engine: "postgres", label: "Analytics warehouse" }
];
export const storageSources: StorageSourceDefinition[] = [
{ key: "(default)", engine: "local", transport: "server" },
{ key: "media", engine: "s3", transport: "server", label: "Public media" }
];

Quindi indirizza una collezione verso una di esse:

import { defineCollection } from "@rebasepro/admin-types";
const pageViewsCollection = defineCollection({
name: "Page Views",
slug: "page_views",
table: "page_views",
dataSource: "analytics",
properties: { /* … */ }
});

…o una proprietà di un file:

coverImage: {
name: "Cover image",
type: "string",
storage: { storageSource: "media", acceptedFiles: ["image/*"] }
}

I nomi delle variabili d’ambiente sono derivati dalla chiave dell’origine, quindi non c’è nulla da mantenere sincronizzato manualmente:

<VARIABLE> the default source DATABASE_URL, S3_BUCKET
<VARIABLE>__<KEY> a named source DATABASE_URL__ANALYTICS, S3_BUCKET__MEDIA

La chiave viene convertita in maiuscolo e i caratteri non alfanumerici diventano trattini bassi (underscore), quindi media-cdn legge S3_BUCKET__MEDIA_CDN.

Il separatore è intenzionalmente un doppio trattino basso. Uno singolo andrebbe in collisione con i nomi di variabili reali: S3_BUCKET_NAME verrebbe interpretato come il bucket per un’origine chiamata name.

DATABASE_URL=postgres://localhost/app
DATABASE_URL__ANALYTICS=postgres://warehouse.internal/analytics
# Optional, per source:
DB_POOL_MAX__ANALYTICS=5
ADMIN_CONNECTION_STRING__ANALYTICS=postgres://…
REBASE_DRIVER__ANALYTICS=@rebasepro/server-postgres

Il driver viene scelto dall’engine dichiarato (postgres e mongodb sono quelli noti) e REBASE_DRIVER__<KEY> lo sovrascrive per qualsiasi altra cosa.

STORAGE_TYPE__MEDIA=s3
S3_BUCKET__MEDIA=my-media-bucket
S3_REGION__MEDIA=eu-central-1
S3_ACCESS_KEY_ID__MEDIA=
S3_SECRET_ACCESS_KEY__MEDIA=

STORAGE_TYPE__<KEY> può essere omesso quando la dichiarazione specifica già l’engine.

Un’origine dati con trasporto server dichiarata ma priva di stringa di connessione fa fallire l’avvio, indicando la variabile da impostare. Questo è intenzionale ed è importante capirne il motivo: l’alternativa è che le collezioni instradate verso l’origine mancante ripieghino silenziosamente sul database predefinito. Ciò significherebbe dati che finiscono nel posto sbagliato dietro un server che si dichiara operativo: molto peggio di un container che si rifiuta di avviarsi.

Anche due chiavi che deriverebbero lo stesso nome di variabile vengono rifiutate, perché una di esse leggerebbe silenziosamente la configurazione dell’altra.

Le origini dichiarate con transport: "direct" vengono ignorate completamente: il client comunica direttamente con esse, quindi il backend non mantiene alcuna connessione e non richiede alcuna configurazione per esse.

Le chiavi di archiviazione condividono un unico namespace piatto e non sono soggette a sicurezza a livello di riga (row-level security), quindi senza un modello esplicito di controllo degli accessi il comportamento predefinito sarebbe “qualsiasi utente autenticato può leggere, sovrascrivere, eliminare o elencare qualsiasi oggetto”. L’ambiente di produzione si rifiuta di avviarsi piuttosto che dare per scontato ciò.

Il modo per definire cosa significa accesso per il tuo progetto è un export storageAuthorize dal pacchetto di configurazione: una funzione, poiché nessuna variabile d’ambiente può esprimere “questo utente può leggere questa chiave”:

// config/index.ts
import type { StorageAuthorize } from "@rebasepro/types";
export const storageAuthorize: StorageAuthorize = async ({ key, user, operation }) => {
if (!user) return false;
const [ownerId] = key.split("/");
return ownerId === user.uid || operation === "read";
};

Esistono due scorciatoie tramite variabili d’ambiente per i casi in cui quello sia realmente il modello adottato:

  • STORAGE_PUBLIC_READ=true — il bucket è una CDN pubblica in sola lettura. Le scritture, le eliminazioni e l’elenco richiedono comunque l’autenticazione.
  • STORAGE_ALLOW_ANY_AUTHENTICATED=true — ogni utente autenticato ha accesso a qualsiasi file. Difendibile per un’applicazione single-tenant, mai per una multi-tenant.

Senza un bucket configurato, l’archiviazione è disattivata in produzione e i caricamenti rispondono con 501. Il disco locale è il filesystem del container, quindi i file scritti lì scompaiono al riavvio successivo: un caricamento che fallisce con un errore chiaro può essere riprovato, uno che ha avuto successo su un disco che sta per essere cancellato no. Imposta FORCE_LOCAL_STORAGE=true solo quando è realmente montato un volume duraturo.

Una conseguenza che vale la pena conoscere se dichiari esplicitamente le origini di archiviazione: non viene creato alcun bucket predefinito al tuo posto. Dichiarare solo un’origine media significa che non esiste alcuna origine (default), e una proprietà che non ne specifica una non ha una destinazione — questo è intenzionale e funziona allo stesso modo sia in sviluppo che in produzione. Dichiara anche (default) se ne desideri una.